diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 84b98fbed..e343ee342 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -21,7 +21,7 @@ pool: variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' - ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)' + ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)' stages: @@ -31,22 +31,22 @@ stages: - job: build steps: - task: UseDotNet@2 - displayName: 'Use .NET 2' # needed for ESRP signing + displayName: 'Use .NET 6' # needed for ESRP signing inputs: - version: 2.x + version: 6.x - task: UseDotNet@2 displayName: 'Use .NET 8' inputs: version: 8.x - - task: PoliCheck@1 + - task: PoliCheck@2 displayName: 'Run PoliCheck "/src"' inputs: inputType: CmdLine cmdLineArgs: '/F:$(Build.SourcesDirectory)/src /T:9 /Sev:"1|2" /PE:2 /O:poli_result_src.xml' - - task: PoliCheck@1 + - task: PoliCheck@2 displayName: 'Run PoliCheck "/test"' inputs: inputType: CmdLine @@ -75,14 +75,14 @@ stages: arguments: '--configuration $(BuildConfiguration) --no-build' # CredScan - - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 + - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 displayName: 'Run CredScan - Src' inputs: toolMajorVersion: 'V2' scanFolder: '$(Build.SourcesDirectory)\src' debugMode: false - - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 + - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 displayName: 'Run CredScan - Test' inputs: toolMajorVersion: 'V2' @@ -95,34 +95,38 @@ stages: FileDirPath: '$(ProductBinPath)' enabled: false - - task: BinSkim@3 + - task: BinSkim@4 displayName: 'Run BinSkim - Product Binaries' inputs: InputType: Basic - AnalyzeTarget: '$(ProductBinPath)\**\Microsoft.OpenApi.dll' + AnalyzeTargetGlob: '$(ProductBinPath)\**\Microsoft.OpenApi.dll' AnalyzeSymPath: '$(ProductBinPath)' AnalyzeVerbose: true AnalyzeHashes: true AnalyzeEnvironment: true - - task: PublishSecurityAnalysisLogs@2 + - task: PublishSecurityAnalysisLogs@3 displayName: 'Publish Security Analysis Logs' inputs: ArtifactName: SecurityLogs - - task: PostAnalysis@1 + - task: PostAnalysis@2 displayName: 'Post Analysis' inputs: BinSkim: true CredScan: true PoliCheck: true - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + - task: EsrpCodeSigning@2 displayName: 'ESRP CodeSigning' inputs: ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' FolderPath: src signConfigType: inlineSignParams + UseMinimatch: true + Pattern: | + **\*.exe + **\*.dll inlineOperation: | [ { @@ -162,26 +166,27 @@ stages: } ] SessionTimeout: 20 - + # Pack - pwsh: dotnet pack $(Build.SourcesDirectory)/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj -o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg displayName: 'pack OpenAPI' - + # Pack - pwsh: dotnet pack $(Build.SourcesDirectory)/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj -o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg displayName: 'pack Readers' # Pack - pwsh: dotnet pack $(Build.SourcesDirectory)/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg - displayName: 'pack Hidi' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + displayName: 'pack Hidi' + + - task: EsrpCodeSigning@2 displayName: 'ESRP CodeSigning Nuget Packages' inputs: ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' FolderPath: '$(Build.ArtifactStagingDirectory)' Pattern: '*.nupkg' signConfigType: inlineSignParams + UseMinimatch: true inlineOperation: | [ { @@ -209,7 +214,7 @@ stages: $xml = [Xml] (Get-Content .\src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj) $version = $xml.Project.PropertyGroup.Version echo $version - echo "##vso[task.setvariable variable=hidiversion]$version" + echo "##vso[task.setvariable variable=hidiversion]$version" # publish hidi as an .exe - task: DotNetCoreCLI@2 @@ -219,7 +224,7 @@ stages: arguments: -c Release --runtime win-x64 /p:PublishSingleFile=true /p:PackAsTool=false --self-contained --output $(Build.ArtifactStagingDirectory)/Microsoft.OpenApi.Hidi-v$(hidiversion) projects: 'src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj' publishWebProjects: False - zipAfterPublish: false + zipAfterPublish: false - task: CopyFiles@2 displayName: Prepare staging folder for upload @@ -236,7 +241,7 @@ stages: - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: Hidi' - inputs: + inputs: ArtifactName: Microsoft.OpenApi.Hidi-v$(hidiversion) PathtoPublish: '$(Build.ArtifactStagingDirectory)/Microsoft.OpenApi.Hidi-v$(hidiversion)' @@ -295,8 +300,8 @@ stages: { "label" : "enhancement", "V2-Enhancement", "displayName" : "Enhancements", "state" : "closed" }, { "label" : "bug", "bug-fix", "displayName" : "Bugs", "state" : "closed" }, { "label" : "documentation", "doc", "displayName" : "Documentation", "state" : "closed"}, - { "label" : "dependencies", "displayName" : "Package Updates", "state" : "closed" }]' - + { "label" : "dependencies", "displayName" : "Package Updates", "state" : "closed" }]' + - deployment: deploy_lib dependsOn: [] environment: nuget-org diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 6e5953f56..145487dab 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.6.0 + uses: dependabot/fetch-metadata@v2.0.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 76130546e..01861ee2d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,6 +2,9 @@ name: CI/CD Pipeline on: [push, pull_request, workflow_dispatch] +permissions: + contents: write + jobs: ci: name: Continuous Integration @@ -49,7 +52,7 @@ jobs: - if: steps.conditionals_handler.outputs.is_default_branch == 'true' name: Bump GH tag id: tag_generator - uses: mathieudutour/github-tag-action@v6.1 + uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} default_bump: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0de297dc4..22eb5f8fa 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,6 +7,11 @@ on: schedule: - cron: '0 8 * * *' +permissions: + contents: read # these permissions are required to run the codeql analysis + actions: read + security-events: write + jobs: analyze: name: CodeQL Analysis @@ -23,7 +28,7 @@ jobs: - name: Initialize CodeQL id: init_codeql - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: queries: security-and-quality @@ -43,6 +48,6 @@ jobs: - name: Perform CodeQL Analysis id: analyze_codeql - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6241be83e..3895975fc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,7 +17,7 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 - name: Login to GitHub package feed - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.1.0 with: username: ${{ secrets.ACR_USERNAME }} password: ${{ secrets.ACR_PASSWORD }} @@ -30,13 +30,13 @@ jobs: id: getversion - name: Push to GitHub Packages - Nightly if: ${{ github.ref == 'refs/heads/vnext' }} - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.3.0 with: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly - name: Push to GitHub Packages - Release if: ${{ github.ref == 'refs/heads/master' }} - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.3.0 with: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.getversion.outputs.version }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 1fec4ad89..de89669f4 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -9,6 +9,11 @@ on: types: [opened, synchronize, reopened] paths-ignore: ['.vscode/**'] + +permissions: + contents: read + pull-requests: read + env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -34,10 +39,6 @@ jobs: with: distribution: 'adopt' java-version: 17 - - name: Setup .NET 5 # At the moment the scanner requires dotnet 5 https://www.nuget.org/packages/dotnet-sonarscanner - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 5.0.x - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -46,14 +47,14 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Cache SonarCloud packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache SonarCloud scanner id: cache-sonar-scanner - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./.sonar/scanner key: ${{ runner.os }}-sonar-scanner diff --git a/.vscode/launch.json b/.vscode/launch.json index acf828db0..c8714e62e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi/bin/Debug/net7.0/Microsoft.OpenApi.Hidi.dll", + "program": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi/bin/Debug/net8.0/Microsoft.OpenApi.Hidi.dll", "args": ["plugin", "-m","C:\\Users\\darrmi\\src\\github\\microsoft\\openapi.net\\test\\Microsoft.OpenApi.Hidi.Tests\\UtilityFiles\\exampleapimanifest.json", "--of","./output"], @@ -28,7 +28,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/Microsoft.OpenApi.WorkBench/bin/Debug/net7.0-windows/Microsoft.OpenApi.Workbench.exe", + "program": "${workspaceFolder}/src/Microsoft.OpenApi.WorkBench/bin/Debug/net8.0-windows/Microsoft.OpenApi.Workbench.exe", "args": [], "cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Workbench", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console diff --git a/Dockerfile b/Dockerfile index e67c8c389..fd821e3e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env WORKDIR /app COPY ./src ./hidi/src @@ -7,10 +7,10 @@ COPY ./README.md ./hidi/README.md WORKDIR /app/hidi RUN dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release -FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runtime +FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime WORKDIR /app -COPY --from=build-env /app/hidi/src/Microsoft.OpenApi.Hidi/bin/Release/net7.0 ./ +COPY --from=build-env /app/hidi/src/Microsoft.OpenApi.Hidi/bin/Release/net8.0 ./ VOLUME /app/output VOLUME /app/openapi.yml diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 09c79d070..8d548ef67 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -9,7 +9,7 @@ enable hidi ./../../artifacts - 1.3.6 + 1.4.0 OpenAPI.NET CLI tool for slicing OpenAPI documents true @@ -30,12 +30,12 @@ - + - + diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index be97e8dc3..c4d34d4cf 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -285,7 +285,7 @@ private static async Task GetOpenApi(HidiOptions options, strin predicate = OpenApiFilterService.CreatePredicate(tags: filterByTags); } - if (requestUrls.Count != 0) + if (requestUrls.Count > 0) { logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection."); predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: document); diff --git a/src/Microsoft.OpenApi.Hidi/readme.md b/src/Microsoft.OpenApi.Hidi/readme.md index 111ee704b..8e89e2a87 100644 --- a/src/Microsoft.OpenApi.Hidi/readme.md +++ b/src/Microsoft.OpenApi.Hidi/readme.md @@ -68,24 +68,28 @@ Used to convert file formats from JSON to YAML and vice versa and performs slici This command accepts the following parameters: • --openapi(-d) - OpenAPI description file path in the local filesystem or a valid URL hosted on a HTTPS server - • --csdl(-cs) - CSDL file path in the local filesystem or a valid URL hosted on a HTTPS server - • --csdlfilter(-csf) - a filter parameter that a user can use to select a subset of a large CSDL file. They do so by providing a comma delimited list of EntitySet and Singleton names that appear in the EntityContainer. - • --output(-o) - Output directory path for the transformed document - • --clean-ouput(-co) - an optional param that allows a user to overwrite an existing file. - • --version(-v) - OpenAPI specification version + • --csdl(--cs) - CSDL file path in the local filesystem or a valid URL hosted on a HTTPS server + • --csdlfilter(--csf) - a filter parameter that a user can use to select a subset of a large CSDL file. They do so by providing a comma delimited list of EntitySet and Singleton names that appear in the EntityContainer. + • --output(-o) - Output directory path for the transformed document. + • --output-folder(--of) - The output directory path for the generated files. + • --clean-ouput(--co) - an optional param that allows a user to overwrite an existing file. + • --version(-v) - OpenAPI specification version. + • --metadata-version(--mv) - the metadata version to use. • --format(-f) - File format - • --loglevel(-ll) - The log level to use when logging messages to the main output - • --inline(-i) - Inline $ref instances - • --resolveExternal(-ex) - Resolve external $refs - • --filterByOperationIds(-op) - Slice document based on OperationId(s) provided. Accepts a comma delimited list of operation ids. + • --terse-output(--to) - Produce terse json output + • --settings-path(--sp) - The configuration file with CSDL conversion settings. + • --loglevel(--ll) - The log level to use when logging messages to the main output + • --inline-local - Inline local $ref instances + • --inline-external(--ex) - Inline external $refs + • --filterByOperationIds(--op) - Slice document based on OperationId(s) provided. Accepts a comma delimited list of operation ids. • --filterByTags(-t) - Slice document based on tag(s) provided. Accepts a comma delimited list of tags. • --filterByCollection(-c) - Slices the OpenAPI document based on the Postman Collection file generated by Resource Explorer - • --filterByManifest (-m) - Slices the OpenAPI document based on the requests defined in the API Manifest file referenced by the provided URI. For API manifests with multiple API Dependenties, use a fragment identifier to select the desired one. e.g ./apimanifest.json#example + • --manifest (-m) - Slices the OpenAPI document based on the requests defined in the API Manifest file referenced by the provided URI. For API manifests with multiple API Dependenties, use a fragment identifier to select the desired one. e.g ./apimanifest.json#example **Examples:** 1. Filtering by OperationIds - hidi transform -d files\People.yml -f yaml -o files\People.yml -v OpenApi3_0 -op users_UpdateInsights -co + hidi transform -d files\People.yml -f yaml -o files\People.yml -v OpenApi3_0 --op users_UpdateInsights --co 2. Filtering by Postman collection hidi transform --openapi files\People.yml --format yaml --output files\People2.yml --version OpenApi3_0 --filterByCollection Graph-Collection-0017059134807617005.postman_collection.json @@ -94,7 +98,7 @@ This command accepts the following parameters: hidi transform --input Files/Todo.xml --output Files/Todo-subset.yml --format yaml --version OpenApi3_0 --filterByOperationIds Todos.Todo.UpdateTodo 4. CSDL Filtering by EntitySets and Singletons - hidi transform -cs dataverse.csdl --csdlFilter "appointments,opportunities" -o appointmentsAndOpportunities.yaml -ll trace + hidi transform --cs dataverse.csdl --csdlFilter "appointments,opportunities" -o appointmentsAndOpportunities.yaml --ll trace Run transform -h to see all the available usage options. diff --git a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj index 0569a7c06..5441dcd61 100644 --- a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj +++ b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj @@ -1,9 +1,9 @@ - + netstandard2.0 latest true - 1.6.11 + 1.6.14 OpenAPI.NET Readers for JSON and YAML documents true @@ -26,7 +26,7 @@ - + diff --git a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj index 75c80bcac..248c60d21 100644 --- a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj +++ b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj index b5db340ba..fc3540a42 100644 --- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj +++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj @@ -3,7 +3,7 @@ netstandard2.0 Latest true - 1.6.11 + 1.6.14 .NET models with JSON and YAML writers for OpenAPI specification true diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 107d9cc15..34b546352 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -605,6 +605,11 @@ public static class OpenApiConstants /// public const string BodyName = "x-bodyName"; + /// + /// Field: Examples Extension + /// + public const string ExamplesExtension = "x-examples"; + /// /// Field: version3_0_0 /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 292a9bf9a..42b6734f7 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; diff --git a/src/Microsoft.OpenApi/Models/OpenApiExample.cs b/src/Microsoft.OpenApi/Models/OpenApiExample.cs index d55c57daa..b32810a64 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiExample.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiExample.cs @@ -121,6 +121,16 @@ public virtual void SerializeAsV3WithoutReference(IOpenApiWriter writer) } internal void SerializeInternalWithoutReference(IOpenApiWriter writer, OpenApiSpecVersion version) + { + Serialize(writer, OpenApiSpecVersion.OpenApi3_0); + } + + /// + /// Writes out existing examples in a mediatype object + /// + /// + /// + public void Serialize(IOpenApiWriter writer, OpenApiSpecVersion version) { writer.WriteStartObject(); diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs index 8c33a4412..048e29cb5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using System.Collections.Generic; using Json.Schema; +using System.Linq; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Helpers; @@ -389,6 +390,20 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) } } + //examples + if (Examples != null && Examples.Any()) + { + writer.WritePropertyName(OpenApiConstants.ExamplesExtension); + writer.WriteStartObject(); + + foreach (var example in Examples) + { + writer.WritePropertyName(example.Key); + example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0); + } + writer.WriteEndObject(); + } + // extensions writer.WriteExtensions(extensionsClone, OpenApiSpecVersion.OpenApi2_0); diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 2ff1d6fd2..00d50a7be 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -100,14 +100,7 @@ private void SerializeInternal(IOpenApiWriter writer, ActionOpenApiRequestBody public OpenApiRequestBody GetEffective(OpenApiDocument doc) { - if (Reference != null) - { - return doc.ResolveReferenceTo(Reference); - } - else - { - return this; - } + return Reference != null ? doc.ResolveReferenceTo(Reference) : this; } /// @@ -173,6 +166,7 @@ internal OpenApiBodyParameter ConvertToBodyParameter() // To allow round-tripping we use an extension to hold the name Name = "body", Schema = Content.Values.FirstOrDefault()?.Schema ?? new JsonSchemaBuilder(), + Examples = Content.Values.FirstOrDefault()?.Examples, Required = Required, Extensions = Extensions.ToDictionary(static k => k.Key, static v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model. }; @@ -206,6 +200,7 @@ internal IEnumerable ConvertToFormDataParameters() Description = property.Value.GetDescription(), Name = property.Key, Schema = property.Value, + Examples = Content.Values.FirstOrDefault()?.Examples, Required = Content.First().Value.Schema.GetRequired().Contains(property.Key) }; } diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index d34e203a9..e0785fe67 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -188,6 +188,22 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteEndObject(); } + if (Content.Values.Any(m => m.Examples != null && m.Examples.Any())) + { + writer.WritePropertyName(OpenApiConstants.ExamplesExtension); + writer.WriteStartObject(); + + foreach (var example in Content + .Where(mediaTypePair => mediaTypePair.Value.Examples != null && mediaTypePair.Value.Examples.Any()) + .SelectMany(mediaTypePair => mediaTypePair.Value.Examples)) + { + writer.WritePropertyName(example.Key); + example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0); + } + + writer.WriteEndObject(); + } + writer.WriteExtensions(mediatype.Value.Extensions, OpenApiSpecVersion.OpenApi2_0); foreach (var key in mediatype.Value.Extensions.Keys) diff --git a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs index e2ec7bdc9..d81bedabb 100644 --- a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs +++ b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs @@ -112,7 +112,7 @@ public static async Task LoadAsync(Stream input, string format, Open bufferedStream.Position = 0; } - using var reader = new StreamReader(bufferedStream); + using var reader = new StreamReader(bufferedStream, default, true, -1, settings.LeaveStreamOpen); return await LoadAsync(reader, format, settings, cancellationToken); } diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs index ad6033a14..6e8d3d53c 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs @@ -199,7 +199,8 @@ internal static OpenApiRequestBody CreateRequestBody( k => k, _ => new OpenApiMediaType { - Schema = bodyParameter.Schema + Schema = bodyParameter.Schema, + Examples = bodyParameter.Examples }), Extensions = bodyParameter.Extensions }; diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs index 07634ddd4..ca1110761 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiParameterDeserializer.cs @@ -122,12 +122,17 @@ internal static partial class OpenApiV2Deserializer "schema", (o, n) => o.Schema = LoadSchema(n) }, + { + "x-examples", + LoadParameterExamplesExtension + }, }; private static readonly PatternFieldMap _parameterPatternFields = new() { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} + {s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase), + (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; private static void LoadStyle(OpenApiParameter p, string v) @@ -158,6 +163,12 @@ private static void LoadStyle(OpenApiParameter p, string v) return; } } + + private static void LoadParameterExamplesExtension(OpenApiParameter parameter, ParseNode node) + { + var examples = LoadExamplesExtension(node); + node.Context.SetTempStorage(TempStorageKeys.Examples, examples, parameter); + } private static JsonSchemaBuilder GetOrCreateParameterSchemaBuilder() { @@ -228,6 +239,14 @@ public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBod node.Context.SetTempStorage("schema", null); } + // load examples from storage and add them to the parameter + var examples = node.Context.GetFromTempStorage>(TempStorageKeys.Examples, parameter); + if (examples != null) + { + parameter.Examples = examples; + node.Context.SetTempStorage("examples", null); + } + var isBodyOrFormData = (bool)node.Context.GetFromTempStorage(TempStorageKeys.ParameterIsBodyOrFormData); if (isBodyOrFormData && !loadRequestBody) { diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs index 8078f7ff6..2b612c488 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using Json.Schema; using Microsoft.OpenApi.Extensions; @@ -30,6 +31,10 @@ internal static partial class OpenApiV2Deserializer "examples", LoadExamples }, + { + "x-examples", + LoadResponseExamplesExtension + }, { "schema", (o, n) => n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n), o) @@ -39,7 +44,8 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _responsePatternFields = new() { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} + {s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase), + (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; private static readonly AnyFieldMap _mediaTypeAnyFields = @@ -71,6 +77,8 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P ?? context.DefaultContentType ?? new List { "application/octet-stream" }; var schema = context.GetFromTempStorage(TempStorageKeys.ResponseSchema, response); + var examples = context.GetFromTempStorage>(TempStorageKeys.Examples, response) + ?? new Dictionary(); foreach (var produce in produces) { @@ -86,7 +94,8 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P { var mediaType = new OpenApiMediaType { - Schema = schema + Schema = schema, + Examples = examples }; response.Content.Add(produce, mediaType); @@ -94,12 +103,55 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P } context.SetTempStorage(TempStorageKeys.ResponseSchema, null, response); + context.SetTempStorage(TempStorageKeys.Examples, null, response); context.SetTempStorage(TempStorageKeys.ResponseProducesSet, true, response); } + private static void LoadResponseExamplesExtension(OpenApiResponse response, ParseNode node) + { + var examples = LoadExamplesExtension(node); + node.Context.SetTempStorage(TempStorageKeys.Examples, examples, response); + } + + private static Dictionary LoadExamplesExtension(ParseNode node) + { + var mapNode = node.CheckMapNode(OpenApiConstants.ExamplesExtension); + var examples = new Dictionary(); + + foreach (var examplesNode in mapNode) + { + // Load the media type node as an OpenApiExample object + var example = new OpenApiExample(); + var exampleNode = examplesNode.Value.CheckMapNode(examplesNode.Name); + foreach (var valueNode in exampleNode) + { + switch (valueNode.Name.ToLowerInvariant()) + { + case "summary": + example.Summary = valueNode.Value.GetScalarValue(); + break; + case "description": + example.Description = valueNode.Value.GetScalarValue(); + break; + case "value": + example.Value = valueNode.Value.CreateAny(); + break; + case "externalValue": + example.ExternalValue = valueNode.Value.GetScalarValue(); + break; + } + } + + examples.Add(examplesNode.Name, example); + } + + return examples; + } + private static void LoadExamples(OpenApiResponse response, ParseNode node) { var mapNode = node.CheckMapNode("examples"); + foreach (var mediaTypeNode in mapNode) { LoadExample(response, mediaTypeNode.Name, mediaTypeNode.Value); @@ -110,10 +162,7 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars { var exampleNode = node.CreateAny(); - if (response.Content == null) - { - response.Content = new Dictionary(); - } + response.Content ??= new Dictionary(); OpenApiMediaType mediaTypeObject; if (response.Content.TryGetValue(mediaType, out var value)) @@ -144,6 +193,7 @@ public static OpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostD } var response = new OpenApiResponse(); + foreach (var property in mapNode) { property.ParseField(response, _responseFixedFields, _responsePatternFields); diff --git a/src/Microsoft.OpenApi/Reader/V2/TempStorageKeys.cs b/src/Microsoft.OpenApi/Reader/V2/TempStorageKeys.cs index 62b6d6663..1d99b1336 100644 --- a/src/Microsoft.OpenApi/Reader/V2/TempStorageKeys.cs +++ b/src/Microsoft.OpenApi/Reader/V2/TempStorageKeys.cs @@ -17,5 +17,6 @@ internal static class TempStorageKeys public const string GlobalConsumes = "globalConsumes"; public const string GlobalProduces = "globalProduces"; public const string ParameterIsBodyOrFormData = "parameterIsBodyOrFormData"; + public const string Examples = "examples"; } } diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index ac0671e1b..8ba63f9ea 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -175,7 +175,7 @@ private OpenApiUrlTreeNode Attach(IEnumerable segments, { if (PathItems.ContainsKey(label)) { - throw new ArgumentException("A duplicate label already exists for this node.", nameof(label)); + throw new ArgumentException($"A duplicate label already exists for this node: {label}", nameof(label)); } Path = currentPath; diff --git a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs index db15b9fec..9f9ce91cd 100644 --- a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs +++ b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs @@ -1,3 +1,4 @@ + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. @@ -293,15 +294,15 @@ private void Validate(object item, Type type) } // Validate unresolved references as references - if (item is IOpenApiReferenceable {UnresolvedReference: true}) + if (item is IOpenApiReferenceable { UnresolvedReference: true }) { type = typeof(IOpenApiReferenceable); } - var rules = _ruleSet.FindRules(type.Name); + var rules = _ruleSet.FindRules(type); foreach (var rule in rules) { - rule.Evaluate(this as IValidationContext, item); + rule.Evaluate(this, item); } } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/JsonSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/JsonSchemaRules.cs index aa9f62ac1..c362f7334 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/JsonSchemaRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/JsonSchemaRules.cs @@ -21,7 +21,7 @@ public static class JsonSchemaRules /// Validate the data matches with the given data type. /// public static ValidationRule SchemaMismatchedDataType => - new ValidationRule( + new ValidationRule(nameof(SchemaMismatchedDataType), (context, jsonSchema) => { // default @@ -79,7 +79,7 @@ public static class JsonSchemaRules /// Validates Schema Discriminator /// public static ValidationRule ValidateSchemaDiscriminator => - new ValidationRule( + new ValidationRule(nameof(ValidateSchemaDiscriminator), (context, jsonSchema) => { // discriminator diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs index f2f3a649c..93eba5c71 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs @@ -24,7 +24,7 @@ public static class OpenApiComponentsRules /// that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$. /// public static ValidationRule KeyMustBeRegularExpression => - new( + new(nameof(KeyMustBeRegularExpression), (context, components) => { ValidateKeys(context, components.Schemas?.Keys, "schemas"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs index cfa8d9927..e31dc1e07 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs @@ -17,7 +17,7 @@ public static class OpenApiContactRules /// Email field MUST be email address. /// public static ValidationRule EmailMustBeEmailFormat => - new( + new(nameof(EmailMustBeEmailFormat), (context, item) => { context.Enter("email"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index fdb6f87b7..c98c9a038 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -17,7 +17,7 @@ public static class OpenApiDocumentRules /// The Info field is required. /// public static ValidationRule OpenApiDocumentFieldIsMissing => - new( + new(nameof(OpenApiDocumentFieldIsMissing), (context, item) => { // info diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs index 5d124e8de..890be82d7 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs @@ -17,7 +17,7 @@ public static class OpenApiExtensibleRules /// Extension name MUST start with "x-". /// public static ValidationRule ExtensionNameMustStartWithXDash => - new( + new(nameof(ExtensionNameMustStartWithXDash), (context, item) => { context.Enter("extensions"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs index ff4fde4a2..1754dbee2 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs @@ -17,7 +17,7 @@ public static class OpenApiExternalDocsRules /// Validate the field is required. /// public static ValidationRule UrlIsRequired => - new( + new(nameof(UrlIsRequired), (context, item) => { // url diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs index 4e07d3ca0..cf983158e 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs @@ -16,7 +16,7 @@ public static class OpenApiHeaderRules /// Validate the data matches with the given data type. /// public static ValidationRule HeaderMismatchedDataType => - new( + new(nameof(HeaderMismatchedDataType), (context, header) => { // example diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs index 88b534c02..337bf8687 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs @@ -17,7 +17,7 @@ public static class OpenApiInfoRules /// Validate the field is required. /// public static ValidationRule InfoRequiredFields => - new( + new(nameof(InfoRequiredFields), (context, item) => { // title diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs index edbf19bf5..08f67d209 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs @@ -17,7 +17,7 @@ public static class OpenApiLicenseRules /// REQUIRED. /// public static ValidationRule LicenseRequiredFields => - new( + new(nameof(LicenseRequiredFields), (context, license) => { context.Enter("name"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs index 998bd778a..3b56bffd7 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs @@ -24,7 +24,7 @@ public static class OpenApiMediaTypeRules /// Validate the data matches with the given data type. /// public static ValidationRule MediaTypeMismatchedDataType => - new( + new(nameof(MediaTypeMismatchedDataType), (context, mediaType) => { // example diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs index de31d933d..c6db49d7c 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs @@ -17,7 +17,7 @@ public static class OpenApiOAuthFlowRules /// Validate the field is required. /// public static ValidationRule OAuthFlowRequiredFields => - new( + new(nameof(OAuthFlowRequiredFields), (context, flow) => { // authorizationUrl diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs index cbeb75e05..512c518ce 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs @@ -17,7 +17,7 @@ public static class OpenApiParameterRules /// Validate the field is required. /// public static ValidationRule ParameterRequiredFields => - new( + new(nameof(ParameterRequiredFields), (context, item) => { // name @@ -43,7 +43,7 @@ public static class OpenApiParameterRules /// Validate the "required" field is true when "in" is path. /// public static ValidationRule RequiredMustBeTrueWhenInIsPath => - new( + new(nameof(RequiredMustBeTrueWhenInIsPath), (context, item) => { // required @@ -62,7 +62,7 @@ public static class OpenApiParameterRules /// Validate the data matches with the given data type. /// public static ValidationRule ParameterMismatchedDataType => - new( + new(nameof(ParameterMismatchedDataType), (context, parameter) => { // example @@ -101,7 +101,7 @@ public static class OpenApiParameterRules /// Validate that a path parameter should always appear in the path /// public static ValidationRule PathParameterShouldBeInThePath => - new( + new(nameof(PathParameterShouldBeInThePath), (context, parameter) => { if (parameter.In == ParameterLocation.Path && diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs index c25ca8aff..9c23f7220 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Properties; @@ -19,7 +18,7 @@ public static class OpenApiPathsRules /// A relative path to an individual endpoint. The field name MUST begin with a slash. /// public static ValidationRule PathNameMustBeginWithSlash => - new( + new(nameof(PathNameMustBeginWithSlash), (context, item) => { foreach (var pathName in item.Keys) @@ -36,12 +35,11 @@ public static class OpenApiPathsRules } }); - private static readonly Regex regexPath = new Regex("\\{([^/]+)\\}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); /// /// A relative path to an individual endpoint. The field name MUST begin with a slash. /// public static ValidationRule PathMustBeUnique => - new ValidationRule( + new ValidationRule(nameof(PathMustBeUnique), (context, item) => { var hashSet = new HashSet(); @@ -50,7 +48,7 @@ public static class OpenApiPathsRules { context.Enter(path); - var pathSignature = regexPath.Replace(path, "{}"); + var pathSignature = GetPathSignature(path); if (!hashSet.Add(pathSignature)) context.CreateError(nameof(PathMustBeUnique), @@ -60,6 +58,28 @@ public static class OpenApiPathsRules } }); + /// + /// Replaces placeholders in the path with {}, e.g. /pets/{petId} becomes /pets/{} . + /// + /// The input path + /// The path signature + private static string GetPathSignature(string path) + { + for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2)) + { + int closeBrace = path.IndexOf('}', openBrace); + + if (closeBrace < 0) + { + return path; + } + + path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace); + } + + return path; + } + // add more rules } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs index 0f725c90e..f30b49ea0 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs @@ -17,7 +17,7 @@ public static class OpenApiResponseRules /// Validate the field is required. /// public static ValidationRule ResponseRequiredFields => - new( + new(nameof(ResponseRequiredFields), (context, response) => { // description diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs index 1afe9a388..a2b91dc31 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Linq; @@ -17,7 +17,7 @@ public static class OpenApiResponsesRules /// An OpenAPI operation must contain at least one response /// public static ValidationRule ResponsesMustContainAtLeastOneResponse => - new( + new(nameof(ResponsesMustContainAtLeastOneResponse), (context, responses) => { if (!responses.Keys.Any()) @@ -31,7 +31,7 @@ public static class OpenApiResponsesRules /// The response key must either be "default" or an HTTP status code (1xx, 2xx, 3xx, 4xx, 5xx). /// public static ValidationRule ResponsesMustBeIdentifiedByDefaultOrStatusCode => - new( + new(nameof(ResponsesMustBeIdentifiedByDefaultOrStatusCode), (context, responses) => { foreach (var key in responses.Keys) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs index 292fd1fd0..dd11a661d 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs @@ -17,7 +17,7 @@ public static class OpenApiServerRules /// Validate the field is required. /// public static ValidationRule ServerRequiredFields => - new( + new(nameof(ServerRequiredFields), (context, server) => { context.Enter("url"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs index f28732e1e..cc006f971 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs @@ -17,7 +17,7 @@ public static class OpenApiTagRules /// Validate the field is required. /// public static ValidationRule TagRequiredFields => - new( + new(nameof(TagRequiredFields), (context, tag) => { context.Enter("name"); diff --git a/src/Microsoft.OpenApi/Validations/ValidationRule.cs b/src/Microsoft.OpenApi/Validations/ValidationRule.cs index aa866734a..bccb28be6 100644 --- a/src/Microsoft.OpenApi/Validations/ValidationRule.cs +++ b/src/Microsoft.OpenApi/Validations/ValidationRule.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; +using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Properties; @@ -17,12 +18,22 @@ public abstract class ValidationRule /// internal abstract Type ElementType { get; } + /// + /// Validation rule Name. + /// + public string Name { get; } + /// /// Validate the object. /// /// The context. /// The object item. internal abstract void Evaluate(IValidationContext context, object item); + + internal ValidationRule(string name) + { + Name = !string.IsNullOrEmpty(name) ? name : throw new ArgumentNullException(nameof(name)); + } } /// @@ -32,14 +43,26 @@ public abstract class ValidationRule public class ValidationRule : ValidationRule { private readonly Action _validate; + + /// + /// Initializes a new instance of the class. + /// + /// Action to perform the validation. + [Obsolete("Please use the other constructor and specify a name")] + public ValidationRule(Action validate) + : this (Guid.NewGuid().ToString("D"), validate) + { + } /// /// Initializes a new instance of the class. /// + /// Validation rule name. /// Action to perform the validation. - public ValidationRule(Action validate) + public ValidationRule(string name, Action validate) + : base(name) { - _validate = Utils.CheckArgumentNull(validate); + _validate = Utils.CheckArgumentNull(validate); } internal override Type ElementType diff --git a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs index af30c04bc..e5950c300 100644 --- a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs +++ b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. + +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -8,6 +9,7 @@ using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Properties; using Microsoft.OpenApi.Validations.Rules; +using System.Data; namespace Microsoft.OpenApi.Validations { @@ -16,16 +18,12 @@ namespace Microsoft.OpenApi.Validations /// public sealed class ValidationRuleSet { - private Dictionary> _rulesDictionary = new(); + private Dictionary> _rulesDictionary = new(); private static ValidationRuleSet _defaultRuleSet; private List _emptyRules = new(); - /// - /// Gets the keys in this rule set. - /// - public ICollection Keys => _rulesDictionary.Keys; /// /// Gets the rules in this rule set. @@ -45,13 +43,13 @@ public ValidationRuleSet() } /// - /// Retrieve the rules that are related to a specific key. + /// Retrieve the rules that are related to a specific type /// - /// The key of the rules to search for. - /// Either the rules related to the given key, or an empty list. - public IList FindRules(string key) + /// The type that is to be validated + /// Either the rules related to the type, or an empty list. + public IList FindRules(Type type) { - _rulesDictionary.TryGetValue(key, out var results); + _rulesDictionary.TryGetValue(type, out var results); return results ?? _emptyRules; } @@ -92,7 +90,7 @@ public static ValidationRuleSet GetEmptyRuleSet() /// The rule set to add validation rules to. /// The validation rules to be added to the rules set. /// Throws a null argument exception if the arguments are null. - public static void AddValidationRules(ValidationRuleSet ruleSet, IDictionary> rules) + public static void AddValidationRules(ValidationRuleSet ruleSet, IDictionary> rules) { if (ruleSet == null || rules == null) { @@ -118,7 +116,7 @@ public ValidationRuleSet(ValidationRuleSet ruleSet) foreach (var rule in ruleSet) { - Add(rule.ElementType.Name, rule); + Add(rule.ElementType, rule); } } @@ -126,7 +124,7 @@ public ValidationRuleSet(ValidationRuleSet ruleSet) /// Initializes a new instance of the class. /// /// Rules to be contained in this ruleset. - public ValidationRuleSet(IDictionary> rules) + public ValidationRuleSet(IDictionary> rules) { if (rules == null) { @@ -144,7 +142,7 @@ public ValidationRuleSet(IDictionary> rules) /// /// The key for the rule. /// The list of rules. - public void Add(string key, IList rules) + public void Add(Type key, IList rules) { foreach (var rule in rules) { @@ -158,7 +156,7 @@ public void Add(string key, IList rules) /// The key for the rule. /// The rule. /// Exception thrown when rule already exists. - public void Add(string key, ValidationRule rule) + public void Add(Type key, ValidationRule rule) { if (!_rulesDictionary.ContainsKey(key)) { @@ -180,7 +178,7 @@ public void Add(string key, ValidationRule rule) /// The new rule. /// The old rule. /// true, if the update was successful; otherwise false. - public bool Update(string key, ValidationRule newRule, ValidationRule oldRule) + public bool Update(Type key, ValidationRule newRule, ValidationRule oldRule) { if (_rulesDictionary.TryGetValue(key, out var currentRules)) { @@ -195,18 +193,33 @@ public bool Update(string key, ValidationRule newRule, ValidationRule oldRule) /// /// The key of the collection of rules to be removed. /// true if the collection of rules with the provided key is removed; otherwise, false. - public bool Remove(string key) + public bool Remove(Type key) { return _rulesDictionary.Remove(key); } + /// + /// Remove a rule by its name from all types it is used by. + /// + /// Name of the rule. + public void Remove(string ruleName) + { + foreach (KeyValuePair> rule in _rulesDictionary) + { + _rulesDictionary[rule.Key] = rule.Value.Where(vr => !vr.Name.Equals(ruleName, StringComparison.Ordinal)).ToList(); + } + + // Remove types with no rule + _rulesDictionary = _rulesDictionary.Where(r => r.Value.Any()).ToDictionary(r => r.Key, r => r.Value); + } + /// /// Removes a rule by key. /// /// The key of the rule to be removed. /// The rule to be removed. /// true if the rule is successfully removed; otherwise, false. - public bool Remove(string key, ValidationRule rule) + public bool Remove(Type key, ValidationRule rule) { if (_rulesDictionary.TryGetValue(key, out IList validationRules)) { @@ -239,7 +252,7 @@ public void Clear() /// /// The key to locate in the rule set. /// true if the rule set contains an element with the key; otherwise, false. - public bool ContainsKey(string key) + public bool ContainsKey(Type key) { return _rulesDictionary.ContainsKey(key); } @@ -250,7 +263,7 @@ public bool ContainsKey(string key) /// The key to locate. /// The rule to locate. /// - public bool Contains(string key, ValidationRule rule) + public bool Contains(Type key, ValidationRule rule) { return _rulesDictionary.TryGetValue(key, out IList validationRules) && validationRules.Contains(rule); } @@ -263,7 +276,7 @@ public bool Contains(string key, ValidationRule rule) /// key is found; otherwise, an empty object. /// This parameter is passed uninitialized. /// true if the specified key has rules. - public bool TryGetValue(string key, out IList rules) + public bool TryGetValue(Type key, out IList rules) { return _rulesDictionary.TryGetValue(key, out rules); } @@ -300,7 +313,7 @@ private static ValidationRuleSet BuildDefaultRuleSet() var propertyValue = property.GetValue(null); // static property if (propertyValue is ValidationRule rule) { - ruleSet.Add(rule.ElementType.Name, rule); + ruleSet.Add(rule.ElementType, rule); } } diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 638e05153..b5f1cf6bc 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -11,12 +11,12 @@ - - + + - - - + + + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 85479d684..63e28a8a9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -17,14 +17,14 @@ - + + + - - - - - + + + diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs index 816a58226..e05c9ba9d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs @@ -34,5 +34,20 @@ public void StreamShouldNotCloseIfLeaveStreamOpenSettingEqualsTrue() _ = OpenApiDocument.Load(stream, "yaml", settings); Assert.True(stream.CanRead); } + + [Fact] + public async void StreamShouldNotBeDisposedIfLeaveStreamOpenSettingIsTrue() + { + var memoryStream = new MemoryStream(); + using var fileStream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml")); + + await fileStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + var stream = memoryStream; + + var result = OpenApiDocument.Load(stream, "yaml", new OpenApiReaderSettings { LeaveStreamOpen = true }); + stream.Seek(0, SeekOrigin.Begin); // does not throw an object disposed exception + Assert.True(stream.CanRead); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs index f77ab0cb0..f264c23f6 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs @@ -9,9 +9,13 @@ using Json.Schema; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Reader.ParseNodes; using Microsoft.OpenApi.Reader.V2; +using Microsoft.OpenApi.Reader.V3; +using Microsoft.OpenApi.Tests; +using Microsoft.OpenApi.Writers; using Xunit; namespace Microsoft.OpenApi.Readers.Tests.V2Tests @@ -293,5 +297,259 @@ public void ParseOperationWithResponseExamplesShouldSucceed() .Excluding(o => o.Responses["200"].Content["application/json"].Example.Node[2].Parent) .Excluding(o => o.Responses["200"].Content["application/json"].Example.Node[2].Root)); } + + [Fact] + public void ParseOperationWithEmptyProducesArraySetsResponseSchemaIfExists() + { + // Arrange + MapNode node; + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "operationWithEmptyProducesArrayInResponse.json")); + node = TestHelper.CreateYamlMapNode(stream); + + // Act + var operation = OpenApiV2Deserializer.LoadOperation(node); + var expected = @"{ + ""produces"": [ + ""application/octet-stream"" + ], + ""responses"": { + ""200"": { + ""description"": ""OK"", + ""schema"": { + ""type"": ""string"", + ""description"": ""The content of the file."", + ""format"": ""binary"", + ""x-ms-summary"": ""File Content"" + } + } + } +}"; + + var stringBuilder = new StringBuilder(); + var jsonWriter = new OpenApiJsonWriter(new StringWriter(stringBuilder)); + operation.SerializeAsV2(jsonWriter); + + // Assert + var actual = stringBuilder.ToString(); + actual.MakeLineBreaksEnvironmentNeutral().Should().BeEquivalentTo(expected.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void ParseOperationWithBodyAndEmptyConsumesSetsRequestBodySchemaIfExists() + { + // Arrange + MapNode node; + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "operationWithBodyAndEmptyConsumes.yaml")); + node = TestHelper.CreateYamlMapNode(stream); + + // Act + var operation = OpenApiV2Deserializer.LoadOperation(node); + + // Assert + operation.Should().BeEquivalentTo(_operationWithBody, options => options.IgnoringCyclicReferences()); + } + + [Fact] + public void ParseV2ResponseWithExamplesExtensionWorks() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "opWithResponseExamplesExtension.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV2Deserializer.LoadOperation(node); + var actual = operation.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + + // Assert + var expected = @"summary: Get all pets +responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + examples: + example1: + summary: Example - List of Pets + value: + - name: Buddy + age: 2 + - name: Whiskers + age: 1 + example2: + summary: Example - Playful Cat + value: + name: Whiskers + age: 1"; + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void LoadV3ExamplesInResponseAsExtensionsWorks() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "v3OperationWithResponseExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV3Deserializer.LoadOperation(node); + var actual = operation.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + + // Assert + var expected = @"summary: Get all pets +produces: + - application/json +responses: + '200': + description: Successful response + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + x-examples: + example1: + summary: Example - List of Pets + value: + - name: Buddy + age: 2 + - name: Whiskers + age: 1 + example2: + summary: Example - Playful Cat + value: + name: Whiskers + age: 1"; + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void LoadV2OperationWithBodyParameterExamplesWorks() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "opWithBodyParameterExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV2Deserializer.LoadOperation(node); + var actual = operation.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + + // Assert + var expected = @"summary: Get all pets +requestBody: + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + examples: + example1: + summary: Example - List of Pets + value: + - name: Buddy + age: 2 + - name: Whiskers + age: 1 + example2: + summary: Example - Playful Cat + value: + name: Whiskers + age: 1 + required: true + x-bodyName: body +responses: { }"; + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void LoadV3ExamplesInRequestBodyParameterAsExtensionsWorks() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "v3OperationWithBodyParameterExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV3Deserializer.LoadOperation(node); + var actual = operation.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + + // Assert + var expected = @"summary: Get all pets +consumes: + - application/json +parameters: + - in: body + name: body + required: true + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + x-examples: + example1: + summary: Example - List of Pets + value: + - name: Buddy + age: 2 + - name: Whiskers + age: 1 + example2: + summary: Example - Playful Cat + value: + name: Whiskers + age: 1 +responses: { }"; + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithBodyParameterExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithBodyParameterExamples.yaml new file mode 100644 index 000000000..e2ffcc7df --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithBodyParameterExamples.yaml @@ -0,0 +1,29 @@ +summary: Get all pets +consumes: + - application/json +parameters: + - in: body + name: body + required: true + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + x-examples: + example1: + summary: Example - List of Pets + value: + - name: Buddy + age: 2 + - name: Whiskers + age: 1 + example2: + summary: Example - Playful Cat + value: + name: Whiskers + age: 1 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithResponseExamplesExtension.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithResponseExamplesExtension.yaml new file mode 100644 index 000000000..5dcc89d97 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/opWithResponseExamplesExtension.yaml @@ -0,0 +1,28 @@ +summary: Get all pets +produces: +- application/json +responses: + '200': + description: Successful response + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + x-examples: + example1: + summary: Example - List of Pets + value: + - name: "Buddy" + age: 2 + - name: "Whiskers" + age: 1 + example2: + summary: Example - Playful Cat + value: + name: "Whiskers" + age: 1 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithBodyParameterExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithBodyParameterExamples.yaml new file mode 100644 index 000000000..a0358125a --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithBodyParameterExamples.yaml @@ -0,0 +1,27 @@ +summary: Get all pets +requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + examples: + example1: + summary: Example - List of Pets + value: + - name: "Buddy" + age: 2 + - name: "Whiskers" + age: 1 + example2: + summary: Example - Playful Cat + value: + name: "Whiskers" + age: 1 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithResponseExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithResponseExamples.yaml new file mode 100644 index 000000000..c3b124685 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/v3OperationWithResponseExamples.yaml @@ -0,0 +1,28 @@ +summary: Get all pets +responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + examples: + example1: + summary: Example - List of Pets + value: + - name: "Buddy" + age: 2 + - name: "Whiskers" + age: 1 + example2: + summary: Example - Playful Cat + value: + name: "Whiskers" + age: 1 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 33bc1c0d5..b337a5166 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -1052,6 +1052,20 @@ public void ParseDocumentWithJsonSchemaReferencesWorks() Assert.Equal(expectedSchema, actualSchema); } + [Fact] + public void ValidateExampleShouldNotHaveDataTypeMismatch() + { + // Act + var result = OpenApiDocument.Load(Path.Combine(SampleFolderPath, "documentWithDateExampleInSchema.yaml"), new OpenApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences + + }); + + // Assert + var warnings = result.OpenApiDiagnostic.Warnings; + Assert.False(warnings.Any()); + } [Fact] public void ParseDocWithRefsUsingProxyReferencesSucceeds() { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithDateExampleInSchema.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithDateExampleInSchema.yaml new file mode 100644 index 000000000..ad8c525cd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithDateExampleInSchema.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Lorem Ipsum + version: 1.0.0 +servers: + - url: http://api.example.com/v1 + description: Lorem Ipsum +paths: + /issues: + get: + summary: Returns a list of issues. + description: Lorem Ipsum + responses: + "200": + description: Lorem Ipsum + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/issueData" + example: + data: + - issuedAt: "2023-10-12" +components: + schemas: + issueData: + type: object + title: Issue Data + description: Information about the issue. + properties: + issuedAt: + type: string + format: date + description: Lorem Ipsum + example: "2023-10-12" \ No newline at end of file diff --git a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj index 6475efa53..b878a8faf 100644 --- a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj +++ b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj @@ -8,12 +8,12 @@ - - - + + + - - + + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 0f087c4aa..6674a13d7 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -8,16 +8,16 @@ - - + + - + - - - - + + + + diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs index c7935e768..310511db8 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs @@ -15,7 +15,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiCallbackTests { public static OpenApiCallback AdvancedCallback = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index d093ee64c..6af4ed8d0 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -23,7 +23,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiDocumentTests { public OpenApiDocumentTests() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs index 6da171ec3..bec6f6b23 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs @@ -18,7 +18,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiExampleTests { public static OpenApiExample AdvancedExample = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs index 4d120531b..d63330a09 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs @@ -14,7 +14,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiHeaderTests { public static OpenApiHeader AdvancedHeader = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs index e930aacb9..4468ed201 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiLinkTests { public static readonly OpenApiLink AdvancedLink = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt index 0542c58ce..744f8451c 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt @@ -3,5 +3,11 @@ "name": "name1", "description": "description1", "required": true, - "type": "string" + "type": "string", + "x-examples": { + "test": { + "summary": "summary3", + "description": "description3" + } + } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt index b80b263d3..26b158865 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaReferenceAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt @@ -1 +1 @@ -{"in":"header","name":"name1","description":"description1","required":true,"type":"string"} \ No newline at end of file +{"in":"header","name":"name1","description":"description1","required":true,"type":"string","x-examples":{"test":{"summary":"summary3","description":"description3"}}} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt index 0542c58ce..744f8451c 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt @@ -3,5 +3,11 @@ "name": "name1", "description": "description1", "required": true, - "type": "string" + "type": "string", + "x-examples": { + "test": { + "summary": "summary3", + "description": "description3" + } + } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt index b80b263d3..26b158865 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.SerializeParameterWithSchemaTypeObjectAsV2JsonWorksAsync_produceTerseOutput=True.verified.txt @@ -1 +1 @@ -{"in":"header","name":"name1","description":"description1","required":true,"type":"string"} \ No newline at end of file +{"in":"header","name":"name1","description":"description1","required":true,"type":"string","x-examples":{"test":{"summary":"summary3","description":"description3"}}} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs index f861e0189..b173f2363 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs @@ -19,7 +19,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiParameterTests { public static OpenApiParameter BasicParameter = new() @@ -293,7 +292,13 @@ public void SerializeAdvancedParameterAsV2JsonWorks() "name": "name1", "description": "description1", "required": true, - "format": "double" + "format": "double", + "x-examples": { + "test": { + "summary": "summary3", + "description": "description3" + } + } } """; diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs index 0e205b71e..93d9f337f 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs @@ -14,7 +14,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiRequestBodyTests { public static OpenApiRequestBody AdvancedRequestBody = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs index 421505ee2..d9006ec09 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs @@ -20,7 +20,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiResponseTests { public static OpenApiResponse BasicResponse = new OpenApiResponse(); diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs index 19bac6305..49a5dcbfd 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiSecuritySchemeTests { public static OpenApiSecurityScheme ApiKeySecurityScheme = new() diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs index e43170f5a..c02f7598c 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs @@ -15,7 +15,6 @@ namespace Microsoft.OpenApi.Tests.Models { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiTagTests { public static OpenApiTag BasicTag = new(); diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiCallbackReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiCallbackReferenceTests.cs index 3e5917932..38a39beae 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiCallbackReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiCallbackReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiCallbackReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiExampleReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiExampleReferenceTests.cs index 0faedb3e7..a48ffd906 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiExampleReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiExampleReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiExampleReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiHeaderReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiHeaderReferenceTests.cs index 46bfdcd39..74d8e5797 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiHeaderReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiHeaderReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiHeaderReferenceTests { private const string OpenApi= @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiLinkReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiLinkReferenceTests.cs index 93e31dc11..9a52b5234 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiLinkReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiLinkReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiLinkReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiParameterReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiParameterReferenceTests.cs index e3583b0bf..8f76fc526 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiParameterReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiParameterReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiParameterReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs index dea2313e5..d4aba67c3 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiPathItemReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiRequestBodyReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiRequestBodyReferenceTests.cs index f443960e3..dd417f093 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiRequestBodyReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiRequestBodyReferenceTests.cs @@ -18,7 +18,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiRequestBodyReferenceTests { private readonly string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiResponseReferenceTest.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiResponseReferenceTest.cs index a905a7b1b..f460374a8 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiResponseReferenceTest.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiResponseReferenceTest.cs @@ -17,7 +17,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiResponseReferenceTest { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSecuritySchemeReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSecuritySchemeReferenceTests.cs index a0bea6e39..dccd41692 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSecuritySchemeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSecuritySchemeReferenceTests.cs @@ -15,7 +15,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiSecuritySchemeReferenceTests { private const string OpenApi = @" diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs index 0b2efe1b0..82f1b27a2 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs @@ -15,7 +15,6 @@ namespace Microsoft.OpenApi.Tests.Models.References { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiTagReferenceTest { private const string OpenApi = @"openapi: 3.0.3 diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 1455b29ca..8cce0b6f5 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -478,6 +478,7 @@ namespace Microsoft.OpenApi.Models public const string Enum = "enum"; public const string Example = "example"; public const string Examples = "examples"; + public const string ExamplesExtension = "x-examples"; public const string ExclusiveMaximum = "exclusiveMaximum"; public const string ExclusiveMinimum = "exclusiveMinimum"; public const string Explode = "explode"; @@ -666,6 +667,7 @@ namespace Microsoft.OpenApi.Models public virtual string Summary { get; set; } public virtual bool UnresolvedReference { get; set; } public virtual Microsoft.OpenApi.Any.OpenApiAny Value { get; set; } + public void Serialize(Microsoft.OpenApi.Writers.IOpenApiWriter writer, Microsoft.OpenApi.OpenApiSpecVersion version) { } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV2WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public virtual void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1606,35 +1608,37 @@ namespace Microsoft.OpenApi.Validations } public abstract class ValidationRule { - protected ValidationRule() { } + public string Name { get; } } public sealed class ValidationRuleSet { public ValidationRuleSet() { } public ValidationRuleSet(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet) { } - public ValidationRuleSet(System.Collections.Generic.IDictionary> rules) { } + public ValidationRuleSet(System.Collections.Generic.IDictionary> rules) { } public int Count { get; } - public System.Collections.Generic.ICollection Keys { get; } public System.Collections.Generic.IList Rules { get; } - public void Add(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } - public void Add(string key, System.Collections.Generic.IList rules) { } + public void Add(System.Type key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public void Add(System.Type key, System.Collections.Generic.IList rules) { } public void Clear() { } - public bool Contains(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } - public bool ContainsKey(string key) { } - public System.Collections.Generic.IList FindRules(string key) { } + public bool Contains(System.Type key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public bool ContainsKey(System.Type key) { } + public System.Collections.Generic.IList FindRules(System.Type type) { } public System.Collections.Generic.IEnumerator GetEnumerator() { } public bool Remove(Microsoft.OpenApi.Validations.ValidationRule rule) { } - public bool Remove(string key) { } - public bool Remove(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } - public bool TryGetValue(string key, out System.Collections.Generic.IList rules) { } - public bool Update(string key, Microsoft.OpenApi.Validations.ValidationRule newRule, Microsoft.OpenApi.Validations.ValidationRule oldRule) { } - public static void AddValidationRules(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet, System.Collections.Generic.IDictionary> rules) { } + public void Remove(string ruleName) { } + public bool Remove(System.Type key) { } + public bool Remove(System.Type key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public bool TryGetValue(System.Type key, out System.Collections.Generic.IList rules) { } + public bool Update(System.Type key, Microsoft.OpenApi.Validations.ValidationRule newRule, Microsoft.OpenApi.Validations.ValidationRule oldRule) { } + public static void AddValidationRules(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet, System.Collections.Generic.IDictionary> rules) { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetDefaultRuleSet() { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetEmptyRuleSet() { } } public class ValidationRule : Microsoft.OpenApi.Validations.ValidationRule { + [System.Obsolete("Please use the other constructor and specify a name")] public ValidationRule(System.Action validate) { } + public ValidationRule(string name, System.Action validate) { } } } namespace Microsoft.OpenApi.Validations.Rules diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs index ba4fae149..d92fe0493 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs @@ -12,7 +12,6 @@ namespace Microsoft.OpenApi.Tests.Services { - [UsesVerify] public class OpenApiUrlTreeNodeTests { private OpenApiDocument OpenApiDocumentSample_1 => new() diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs index 1fc8206d9..e5fcc346f 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs @@ -30,12 +30,13 @@ public void ResponseMustHaveADescription() Title = "foo", Version = "1.2.2" }; - openApiDocument.Paths = new(); - openApiDocument.Paths.Add( - "/test", - new() + openApiDocument.Paths = new() + { { - Operations = + "/test", + new() + { + Operations = { [OperationType.Get] = new() { @@ -45,7 +46,9 @@ public void ResponseMustHaveADescription() } } } - }); + } + } + }; var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); @@ -98,8 +101,8 @@ public void ValidateCustomExtension() { var ruleset = ValidationRuleSet.GetDefaultRuleSet(); - ruleset.Add(typeof(OpenApiAny).Name, - new ValidationRule( + ruleset.Add(typeof(OpenApiAny), + new ValidationRule("FooExtensionRule", (context, item) => { if (item.Node["Bar"].ToString() == "hey") @@ -138,6 +141,44 @@ public void ValidateCustomExtension() new OpenApiValidatorError("FooExtensionRule", "#/info/x-foo", "Don't say hey") }); } + + [Fact] + public void RemoveRuleByName_Invalid() + { + Assert.Throws(() => new ValidationRule(null, (vc, oaa) => { })); + Assert.Throws(() => new ValidationRule(string.Empty, (vc, oaa) => { })); + } + + [Fact] + public void RemoveRuleByName() + { + var ruleset = ValidationRuleSet.GetDefaultRuleSet(); + int expected = ruleset.Rules.Count - 1; + ruleset.Remove("KeyMustBeRegularExpression"); + + Assert.Equal(expected, ruleset.Rules.Count); + + ruleset.Remove("KeyMustBeRegularExpression"); + ruleset.Remove("UnknownName"); + + Assert.Equal(expected, ruleset.Rules.Count); + } + + [Fact] + public void RemoveRuleByType() + { + var ruleset = ValidationRuleSet.GetDefaultRuleSet(); + int expected = ruleset.Rules.Count - 1; + + ruleset.Remove(typeof(OpenApiComponents)); + + Assert.Equal(expected, ruleset.Rules.Count); + + ruleset.Remove(typeof(OpenApiComponents)); + ruleset.Remove(typeof(int)); + + Assert.Equal(expected, ruleset.Rules.Count); + } } internal class FooExtension : IOpenApiExtension, IOpenApiElement diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs index 23a0a3e0f..6d0282748 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs @@ -45,5 +45,38 @@ public void ValidatePathsAreUnique() errors.Should().NotBeEmpty(); errors.Select(e => e.Message).Should().BeEquivalentTo(error); } + [Fact] + public void ValidatePathsAreUniqueDoesNotConsiderMultiParametersAsIdentical() + { + // Arrange + var paths = new OpenApiPaths + { + {"/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/{workbookChart-id}/image(width={width},height={height},fittingMode='{fittingMode}')",new OpenApiPathItem() }, + {"/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/{workbookChart-id}/image(width={width},height={height})",new OpenApiPathItem() }, + {"/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/{workbookChart-id}/image(width={width})", new OpenApiPathItem() }, + }; + + // Act + var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().BeEmpty(); + } + [Fact] + public void ValidatePathsAreUniqueConsidersMultiParametersAsIdentical() + { + // Arrange + var paths = new OpenApiPaths + { + {"/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/{workbookChart-id}/image(width={width},height={height})",new OpenApiPathItem() }, + {"/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/{workbookChart-id}/image(width={width},height={size})",new OpenApiPathItem() }, + }; + + // Act + var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().NotBeEmpty(); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs index 2ba962ca5..e011d80ee 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs @@ -1,6 +1,7 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Linq; using Json.Schema; @@ -56,9 +57,9 @@ public void ReferencedSchemaShouldOnlyBeValidatedOnce() }; // Act - var rules = new Dictionary>() + var rules = new Dictionary>() { - { typeof(JsonSchema).Name, + { typeof(JsonSchema), new List() { new AlwaysFailRule() } } }; @@ -106,9 +107,9 @@ public void UnresolvedSchemaReferencedShouldNotBeValidated() }; // Act - var rules = new Dictionary>() + var rules = new Dictionary>() { - { typeof(JsonSchema).Name, + { typeof(JsonSchema), new List() { new AlwaysFailRule() } } }; @@ -122,7 +123,7 @@ public void UnresolvedSchemaReferencedShouldNotBeValidated() public class AlwaysFailRule : ValidationRule { - public AlwaysFailRule() : base((c, _) => c.CreateError("x", "y")) + public AlwaysFailRule() : base("AlwaysFailRule", (c, _) => c.CreateError("x", "y")) { } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index 55ae552d1..4bc7e7cfd 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi.Models; @@ -10,24 +11,24 @@ namespace Microsoft.OpenApi.Validations.Tests { public class ValidationRuleSetTests { - private readonly ValidationRule _contactValidationRule = new ValidationRule( + private readonly ValidationRule _contactValidationRule = new ValidationRule(nameof(_contactValidationRule), (context, item) => { }); - private readonly ValidationRule _headerValidationRule = new ValidationRule( + private readonly ValidationRule _headerValidationRule = new ValidationRule(nameof(_headerValidationRule), (context, item) => { }); - private readonly ValidationRule _parameterValidationRule = new ValidationRule( + private readonly ValidationRule _parameterValidationRule = new ValidationRule(nameof(_parameterValidationRule), (context, item) => { }); - private readonly IDictionary> _rulesDictionary; + private readonly IDictionary> _rulesDictionary; public ValidationRuleSetTests() { - _rulesDictionary = new Dictionary>() + _rulesDictionary = new Dictionary>() { - {"contact", new List { _contactValidationRule } }, - {"header", new List { _headerValidationRule } }, - {"parameter", new List { _parameterValidationRule } } + {typeof(OpenApiContact), new List { _contactValidationRule } }, + {typeof(OpenApiHeader), new List { _headerValidationRule } }, + {typeof(OpenApiParameter), new List { _parameterValidationRule } } }; } @@ -41,7 +42,7 @@ public void RuleSetConstructorsReturnsTheCorrectRules() var ruleSet_4 = new ValidationRuleSet(); // Assert - Assert.NotNull(ruleSet_1?.Rules); + Assert.NotNull(ruleSet_1?.Rules); Assert.NotNull(ruleSet_2?.Rules); Assert.NotNull(ruleSet_3?.Rules); Assert.NotNull(ruleSet_4); @@ -62,7 +63,7 @@ public void RemoveValidatioRuleGivenTheValidationRuleWorks() { // Arrange var ruleSet = new ValidationRuleSet(_rulesDictionary); - var responseValidationRule = new ValidationRule((context, item) => { }); + var responseValidationRule = new ValidationRule("ValidateResponses", (context, item) => { }); // Act and Assert Assert.True(ruleSet.Remove(_contactValidationRule)); @@ -77,9 +78,9 @@ public void RemoveValidationRuleGivenTheKeyAndValidationRuleWorks() var ruleSet = new ValidationRuleSet(_rulesDictionary); // Act - ruleSet.Remove("contact", _contactValidationRule); - ruleSet.Remove("parameter", _headerValidationRule); // validation rule not in parameter key; shouldn't remove - ruleSet.Remove("foo", _parameterValidationRule); // key does not exist; shouldn't remove + ruleSet.Remove(typeof(OpenApiContact), _contactValidationRule); + ruleSet.Remove("parameter"); // validation rule not in parameter key; shouldn't remove + ruleSet.Remove("foo"); // key does not exist; shouldn't remove var rules = ruleSet.Rules; @@ -94,16 +95,16 @@ public void RemoveRulesGivenAKeyWorks() { // Arrange var ruleSet = new ValidationRuleSet(_rulesDictionary); - var responseValidationRule = new ValidationRule((context, item) => { }); - ruleSet.Add("response", new List { responseValidationRule }); - Assert.True(ruleSet.ContainsKey("response")); + var responseValidationRule = new ValidationRule("ValidateResponses", (context, item) => { }); + ruleSet.Add(typeof(OpenApiResponse), new List { responseValidationRule }); + Assert.True(ruleSet.ContainsKey(typeof(OpenApiResponse))); Assert.True(ruleSet.Rules.Contains(responseValidationRule)); // guard // Act - ruleSet.Remove("response"); + ruleSet.Remove(typeof(OpenApiResponse)); // Assert - Assert.False(ruleSet.ContainsKey("response")); + Assert.False(ruleSet.ContainsKey(typeof(OpenApiResponse))); } [Fact] @@ -111,24 +112,24 @@ public void AddNewValidationRuleWorks() { // Arrange var ruleSet = new ValidationRuleSet(_rulesDictionary); - var responseValidationRule = new ValidationRule((context, item) => { }); - var tagValidationRule = new ValidationRule((context, item) => { }); - var pathsValidationRule = new ValidationRule((context, item) => { }); + var responseValidationRule = new ValidationRule("ValidateResponses", (context, item) => { }); + var tagValidationRule = new ValidationRule("ValidateTags", (context, item) => { }); + var pathsValidationRule = new ValidationRule("ValidatePaths", (context, item) => { }); // Act - ruleSet.Add("response", new List { responseValidationRule }); - ruleSet.Add("tag", new List { tagValidationRule }); - var rulesDictionary = new Dictionary>() + ruleSet.Add(typeof(OpenApiResponse), new List { responseValidationRule }); + ruleSet.Add(typeof(OpenApiTag), new List { tagValidationRule }); + var rulesDictionary = new Dictionary>() { - {"paths", new List { pathsValidationRule } } + {typeof(OpenApiPaths), new List { pathsValidationRule } } }; ValidationRuleSet.AddValidationRules(ruleSet, rulesDictionary); - + // Assert - Assert.True(ruleSet.ContainsKey("response")); - Assert.True(ruleSet.ContainsKey("tag")); - Assert.True(ruleSet.ContainsKey("paths")); + Assert.True(ruleSet.ContainsKey(typeof(OpenApiResponse))); + Assert.True(ruleSet.ContainsKey(typeof(OpenApiTag))); + Assert.True(ruleSet.ContainsKey(typeof(OpenApiPaths))); Assert.True(ruleSet.Rules.Contains(responseValidationRule)); Assert.True(ruleSet.Rules.Contains(tagValidationRule)); Assert.True(ruleSet.Rules.Contains(pathsValidationRule)); @@ -139,16 +140,16 @@ public void UpdateValidationRuleWorks() { // Arrange var ruleSet = new ValidationRuleSet(_rulesDictionary); - var responseValidationRule = new ValidationRule((context, item) => { }); - ruleSet.Add("response", new List { responseValidationRule }); + var responseValidationRule = new ValidationRule("ValidateResponses", (context, item) => { }); + ruleSet.Add(typeof(OpenApiResponse), new List { responseValidationRule }); // Act - var pathsValidationRule = new ValidationRule((context, item) => { }); - ruleSet.Update("response", pathsValidationRule, responseValidationRule); + var pathsValidationRule = new ValidationRule("ValidatePaths", (context, item) => { }); + ruleSet.Update(typeof(OpenApiResponse), pathsValidationRule, responseValidationRule); // Assert - Assert.True(ruleSet.Contains("response", pathsValidationRule)); - Assert.False(ruleSet.Contains("response", responseValidationRule)); + Assert.True(ruleSet.Contains(typeof(OpenApiResponse), pathsValidationRule)); + Assert.False(ruleSet.Contains(typeof(OpenApiResponse), responseValidationRule)); } [Fact] @@ -158,8 +159,8 @@ public void TryGetValueWorks() var ruleSet = new ValidationRuleSet(_rulesDictionary); // Act - ruleSet.TryGetValue("contact", out var validationRules); - + ruleSet.TryGetValue(typeof(OpenApiContact), out var validationRules); + // Assert Assert.True(validationRules.Any()); Assert.True(validationRules.Contains(_contactValidationRule)); @@ -170,12 +171,12 @@ public void ClearAllRulesWorks() { // Arrange var ruleSet = new ValidationRuleSet(); - var tagValidationRule = new ValidationRule((context, item) => { }); - var pathsValidationRule = new ValidationRule((context, item) => { }); - var rulesDictionary = new Dictionary>() + var tagValidationRule = new ValidationRule("ValidateTags", (context, item) => { }); + var pathsValidationRule = new ValidationRule("ValidatePaths", (context, item) => { }); + var rulesDictionary = new Dictionary>() { - {"paths", new List { pathsValidationRule } }, - {"tag", new List { tagValidationRule } } + {typeof(OpenApiPaths), new List { pathsValidationRule } }, + {typeof(OpenApiTag), new List { tagValidationRule } } }; ValidationRuleSet.AddValidationRules(ruleSet, rulesDictionary); diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs index 784750ab6..11b429300 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs @@ -7,7 +7,11 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text; using FluentAssertions; +using Json.Schema; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; using Newtonsoft.Json; using Xunit; @@ -265,5 +269,22 @@ public void WriteDateTimeAsJsonShouldMatchExpected(DateTimeOffset dateTimeOffset // Assert writtenString.Should().Be(expectedString); } + + [Fact] + public void OpenApiJsonWriterOutputsValidJsonValueWhenSchemaHasNanOrInfinityValues() + { + // Arrange + var schema = new JsonSchemaBuilder().Enum("NaN", "Infinity", "-Infinity"); + + // Act + var schemaBuilder = new StringBuilder(); + var jsonWriter = new OpenApiJsonWriter(new StringWriter(schemaBuilder)); + jsonWriter.WriteJsonSchema(schema, OpenApiSpecVersion.OpenApi3_0); + var jsonString = schemaBuilder.ToString(); + + // Assert + var exception = Record.Exception(() => System.Text.Json.JsonSerializer.Deserialize>(jsonString)); + Assert.Null(exception); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs index 633610aca..6e1a883c4 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs @@ -18,7 +18,6 @@ namespace Microsoft.OpenApi.Tests.Writers { [Collection("DefaultSettings")] - [UsesVerify] public class OpenApiWriterAnyExtensionsTests { static bool[] shouldProduceTerseOutputValues = new[] { true, false };