From 82d244e1d3acbd89bc278e4e1ac42c1db6f6903f Mon Sep 17 00:00:00 2001 From: Fabian Niederer Date: Mon, 1 Feb 2021 17:55:34 +0100 Subject: [PATCH 1/2] added OpenAPI Diff --- Microsoft.OpenApi.sln | 14 + .../BusinessObjects/ChangeBO.cs | 34 + .../BusinessObjects/ChangedAPIResponseBO.cs | 59 ++ .../BusinessObjects/ChangedBO.cs | 78 ++ .../BusinessObjects/ChangedContentBO.cs | 55 ++ .../BusinessObjects/ChangedEnumBO.cs | 25 + .../BusinessObjects/ChangedExtensionsBO.cs | 49 ++ .../BusinessObjects/ChangedHeaderBO.cs | 78 ++ .../BusinessObjects/ChangedHeadersBO.cs | 52 ++ .../BusinessObjects/ChangedInfoBO.cs | 32 + .../BusinessObjects/ChangedInfosBO.cs | 11 + .../BusinessObjects/ChangedListBO.cs | 67 ++ .../BusinessObjects/ChangedMaxLengthBO.cs | 47 ++ .../BusinessObjects/ChangedMediaTypeBO.cs | 44 ++ .../BusinessObjects/ChangedMetadataBO.cs | 36 + .../BusinessObjects/ChangedOAuthFlowBO.cs | 62 ++ .../BusinessObjects/ChangedOAuthFlowsBO.cs | 50 ++ .../BusinessObjects/ChangedOneOfSchemaBO.cs | 52 ++ .../BusinessObjects/ChangedOpenApiBO.cs | 64 ++ .../BusinessObjects/ChangedOperationBO.cs | 92 +++ .../BusinessObjects/ChangedParameterBO.cs | 92 +++ .../BusinessObjects/ChangedParametersBO.cs | 54 ++ .../BusinessObjects/ChangedPathBO.cs | 61 ++ .../BusinessObjects/ChangedPathsBO.cs | 53 ++ .../BusinessObjects/ChangedReadOnlyBO.cs | 55 ++ .../BusinessObjects/ChangedRequestBodyBO.cs | 60 ++ .../BusinessObjects/ChangedRequiredBO.cs | 25 + .../BusinessObjects/ChangedResponseBO.cs | 49 ++ .../BusinessObjects/ChangedSchemaBO.cs | 119 +++ .../ChangedSecurityRequirementBO.cs | 50 ++ .../ChangedSecurityRequirementsBO.cs | 54 ++ .../ChangedSecuritySchemeBO.cs | 90 +++ .../ChangedSecuritySchemeScopesBO.cs | 19 + .../BusinessObjects/ChangedWriteOnlyBO.cs | 55 ++ .../BusinessObjects/ComposedChangedBO.cs | 61 ++ .../BusinessObjects/DiffContextBO.cs | 91 +++ .../BusinessObjects/DiffResultBO.cs | 39 + .../BusinessObjects/EndpointBO.cs | 18 + .../Compare/ApiResponseDiff.cs | 46 ++ .../Compare/CacheKey.cs | 48 ++ .../Compare/ContentDiff.cs | 61 ++ .../Compare/ExtensionDiff.cs | 20 + .../Compare/ExtensionsDiff.cs | 93 +++ .../Compare/HeaderDiff.cs | 64 ++ .../Compare/HeadersDiff.cs | 43 ++ .../Compare/IExtensionDiff.cs | 17 + .../Compare/ListDiff.cs | 42 ++ .../Compare/MapKeyDiff.cs | 51 ++ .../Compare/MetadataDiff.cs | 25 + .../Compare/OAuthFlowDiff.cs | 32 + .../Compare/OAuthFlowsDiff.cs | 41 ++ .../Compare/OpenApiDiff.cs | 166 +++++ .../Compare/OperationDiff.cs | 97 +++ .../Compare/ParameterDiff.cs | 65 ++ .../Compare/ParametersDiff.cs | 70 ++ .../Compare/PathDiff.cs | 48 ++ .../Compare/PathsDiff.cs | 77 ++ .../Compare/ReferenceDiffCache.cs | 53 ++ .../Compare/RequestBodyDiff.cs | 87 +++ .../Compare/ResponseDiff.cs | 52 ++ .../Compare/SchemaDiff.cs | 374 ++++++++++ .../SchemaDiffResult/ArraySchemaDiffResult.cs | 37 + .../ComposedSchemaDiffResult.cs | 125 ++++ .../SchemaDiffResult/SchemaDiffResult.cs | 168 +++++ .../Compare/SecurityRequirementDiff.cs | 110 +++ .../Compare/SecurityRequirementsDiff.cs | 98 +++ .../Compare/SecuritySchemeDiff.cs | 118 +++ .../Enums/ChangedElementTypeEnum.cs | 31 + .../Enums/DiffResultEnum.cs | 11 + .../Enums/RefTypeEnum.cs | 12 + .../Enums/SchemaTypeEnum.cs | 9 + src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs | 9 + .../Extensions/IOpenApiPrimitiveExtensions.cs | 17 + .../Extensions/ListExtensions.cs | 32 + .../Extensions/OpenApiSchemaExtensions.cs | 22 + .../Extensions/PathExtensions.cs | 28 + src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs | 12 + .../Microsoft.OpenApi.Diff.csproj | 44 ++ src/Microsoft.OpenApi.Diff/OpenApiCompare.cs | 50 ++ .../Output/BaseRenderer.cs | 70 ++ .../Output/ConsoleRender.cs | 346 +++++++++ .../Output/Html/HtmlRender.cs | 32 + .../Output/Html/IHtmlRender.cs | 6 + .../Output/Html/Views/ChangeDetail.cshtml | 31 + .../Views/ChangedOperationOverview.cshtml | 19 + .../Output/Html/Views/Index.cshtml | 185 +++++ .../Html/Views/OperationOverview.cshtml | 14 + .../Output/IConsoleRender.cs | 6 + src/Microsoft.OpenApi.Diff/Output/IRender.cs | 10 + .../Output/Markdown/IMarkdownRender.cs | 6 + .../Output/Markdown/MarkdownRender.cs | 144 ++++ .../Output/RenderViewModel.cs | 50 ++ .../Utils/ChangedUtils.cs | 23 + src/Microsoft.OpenApi.Diff/Utils/Copy.cs | 12 + .../Utils/EndpointUtils.cs | 61 ++ .../Utils/RefPointer.cs | 85 +++ .../ITestUtils.cs | 16 + .../Microsoft.OpenApi.Diff.Tests.csproj | 161 ++++ .../Resources/add-prop-1.yaml | 71 ++ .../Resources/add-prop-2.yaml | 69 ++ .../Resources/allOf_diff_1.yaml | 129 ++++ .../Resources/allOf_diff_2.yaml | 127 ++++ .../Resources/allOf_diff_3.yaml | 126 ++++ .../Resources/allOf_diff_4.yaml | 129 ++++ .../Resources/array_diff_1.yaml | 133 ++++ .../Resources/array_diff_2.yaml | 132 ++++ .../Resources/backwardCompatibility/bc_1.yaml | 134 ++++ .../Resources/backwardCompatibility/bc_2.yaml | 152 ++++ .../Resources/backwardCompatibility/bc_3.yaml | 168 +++++ .../Resources/backwardCompatibility/bc_4.yaml | 157 ++++ .../Resources/backwardCompatibility/bc_5.yaml | 133 ++++ .../Resources/composed_schema_1.yaml | 123 ++++ .../Resources/composed_schema_2.yaml | 129 ++++ .../Resources/content_diff_1.yaml | 32 + .../Resources/content_diff_2.yaml | 47 ++ .../Resources/header_1.yaml | 131 ++++ .../Resources/header_2.yaml | 131 ++++ .../Resources/oneOf_diff_1.yaml | 134 ++++ .../Resources/oneOf_diff_2.yaml | 136 ++++ .../Resources/oneOf_diff_3.yaml | 136 ++++ .../oneOf_discriminator-changed_1.yaml | 49 ++ .../oneOf_discriminator-changed_2.yaml | 49 ++ .../Resources/parameters_diff.yaml | 185 +++++ .../Resources/parameters_diff_1.yaml | 185 +++++ .../Resources/parameters_diff_2.yaml | 183 +++++ .../Resources/path_1.yaml | 35 + .../Resources/path_2.yaml | 35 + .../Resources/path_3.yaml | 52 ++ .../Resources/petstore_v2_1.yaml | 670 +++++++++++++++++ .../Resources/petstore_v2_2.yaml | 686 ++++++++++++++++++ .../Resources/petstore_v2_empty.yaml | 167 +++++ .../Resources/recursive_model_1.yaml | 30 + .../Resources/recursive_model_2.yaml | 28 + .../Resources/recursive_model_3.yaml | 29 + .../Resources/recursive_model_4.yaml | 27 + .../Resources/request_diff_1.yaml | 162 +++++ .../Resources/request_diff_2.yaml | 162 +++++ .../Resources/schema_diff_cache_1.yaml | 173 +++++ .../Resources/security_diff_1.yaml | 240 ++++++ .../Resources/security_diff_2.yaml | 266 +++++++ .../Resources/security_diff_3.yaml | 241 ++++++ .../Microsoft.OpenApi.Diff.Tests/TestUtils.cs | 60 ++ .../Tests/AddPropDiffTest.cs | 23 + .../Tests/AllOfDiffTest.cs | 37 + .../Tests/ArrayDiffTest.cs | 23 + .../Tests/BackwardCompatibilityTest.cs | 56 ++ .../Tests/ContentDiffTest.cs | 32 + .../Tests/OneOfDiffTest.cs | 47 ++ .../Tests/OpenApiDiffTest.cs | 126 ++++ .../Tests/ParameterDiffTest.cs | 17 + .../Tests/PathDiffTest.cs | 25 + .../Tests/RecursiveSchemaTest.cs | 37 + .../Tests/ReferenceDiffCacheTest.cs | 16 + .../Tests/RequestDiffTest.cs | 17 + .../Tests/ResponseHeaderDiffTest.cs | 34 + .../Tests/SecurityDiffTest.cs | 93 +++ .../_Base/BaseTest.cs | 41 ++ 157 files changed, 12925 insertions(+) create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs create mode 100644 src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj create mode 100644 src/Microsoft.OpenApi.Diff/OpenApiCompare.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/IRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/Copy.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs diff --git a/Microsoft.OpenApi.sln b/Microsoft.OpenApi.sln index e64ff3a24..6c5e85275 100644 --- a/Microsoft.OpenApi.sln +++ b/Microsoft.OpenApi.sln @@ -28,6 +28,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.SmokeTest EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Tool", "src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj", "{254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Diff", "src\Microsoft.OpenApi.Diff\Microsoft.OpenApi.Diff.csproj", "{35F25729-19A7-43BA-AAB5-0859EC524F6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.OpenApi.Diff.Tests", "test\Microsoft.OpenApi.Diff.Tests\Microsoft.OpenApi.Diff.Tests.csproj", "{8491A9E6-4F63-4F55-9DFB-AEFE60602351}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +66,14 @@ Global {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.Build.0 = Release|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Release|Any CPU.Build.0 = Release|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +86,8 @@ Global {1ED3C2C1-E1E7-4925-B4E6-2D969C3F5237} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {AD79B61D-88CF-497C-9ED5-41AE3867C5AC} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE} = {E546B92F-20A8-49C3-8323-4B25BB78F3E1} + {35F25729-19A7-43BA-AAB5-0859EC524F6F} = {E546B92F-20A8-49C3-8323-4B25BB78F3E1} + {8491A9E6-4F63-4F55-9DFB-AEFE60602351} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F171EFC-0DB5-4B10-ABFA-AF48D52CC565} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs new file mode 100644 index 000000000..8d254e02b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs @@ -0,0 +1,34 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangeBO + where T : class + { + public T OldValue { get; } + public T NewValue { get; } + public TypeEnum Type { get; } + + private ChangeBO(T oldValue, T newValue, TypeEnum type) + { + OldValue = oldValue; + NewValue = newValue; + Type = type; + } + + public static ChangeBO Changed(T oldValue, T newValue) + { + return new ChangeBO(oldValue, newValue, TypeEnum.Changed); + } + + public static ChangeBO Added(T newValue) + { + return new ChangeBO(null, newValue, TypeEnum.Added); + } + + public static ChangeBO Removed(T oldValue) + { + return new ChangeBO(oldValue, null, TypeEnum.Removed); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs new file mode 100644 index 000000000..a4a60f96a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedAPIResponseBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Response; + + private readonly OpenApiResponses _oldApiResponses; + private readonly OpenApiResponses _newApiResponses; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedAPIResponseBO(OpenApiResponses oldApiResponses, OpenApiResponses newApiResponses, DiffContextBO context) + { + _oldApiResponses = oldApiResponses; + _newApiResponses = newApiResponses; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs new file mode 100644 index 000000000..aa929b204 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ChangedBO + { + protected ChangedBO() + { + } + + protected abstract ChangedElementTypeEnum GetElementType(); + protected string GetIdentifier(string identifier) => identifier ?? GetElementType().GetDisplayName(); + + public abstract DiffResultBO IsChanged(); + + public virtual DiffResultBO IsCoreChanged() => IsChanged(); + + protected abstract List GetCoreChanges(); + + public ChangedInfosBO GetCoreChangeInfo(string identifier, List parentPath = null) + { + var isChanged = IsCoreChanged(); + var newPath = new List(); + + if (!parentPath.IsNullOrEmpty()) + newPath = new List(parentPath); + + newPath.Add(GetIdentifier(identifier)); + + var result = new ChangedInfosBO + { + Path = newPath, + ChangeType = isChanged + }; + + if (isChanged.IsUnchanged()) + return result; + + result.Changes = GetCoreChanges(); + return result; + } + + public virtual List GetAllChangeInfoFlat(string identifier, List parentPath = null) + { + return new List + { + GetCoreChangeInfo(identifier, parentPath) + }; + } + + public static DiffResultBO Result(ChangedBO changed) + { + return changed?.IsChanged() ?? new DiffResultBO(DiffResultEnum.NoChanges); + } + public bool IsCompatible() + { + return IsChanged().IsCompatible(); + } + + public bool IsIncompatible() + { + return IsChanged().IsIncompatible(); + } + + public bool IsUnchanged() + { + return IsChanged().IsUnchanged(); + } + + public bool IsDifferent() + { + return IsChanged().IsDifferent(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs new file mode 100644 index 000000000..3ddee041d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedContentBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Content; + + private readonly Dictionary _oldContent; + private readonly Dictionary _newContent; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedContentBO(Dictionary oldContent, Dictionary newContent, DiffContextBO context) + { + _oldContent = oldContent; + _newContent = newContent; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest && Missing.IsNullOrEmpty() || _context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs new file mode 100644 index 000000000..c5cedb49c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedEnumBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Enum; + + public ChangedEnumBO(IList oldValue, IList newValue, DiffContextBO context) : base(oldValue, newValue, context) + { + } + + public override DiffResultBO IsItemsChanged() + { + if (Context.IsRequest && Missing.IsNullOrEmpty() + || Context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs new file mode 100644 index 000000000..ebed603e4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedExtensionsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Extension; + + private readonly Dictionary _oldExtensions; + private readonly Dictionary _newExtensions; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedExtensionsBO(Dictionary oldExtensions, Dictionary newExtensions, DiffContextBO context) + { + _oldExtensions = oldExtensions; + _newExtensions = newExtensions; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>() + .Concat(Increased.Select(x => (x.Key, (ChangedBO)x.Value))) + .Concat(Missing.Select(x => (x.Key, (ChangedBO)x.Value))) + .Concat(Changed.Select(x => (x.Key, (ChangedBO)x.Value))) + .Where(x => x.Item2 != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs new file mode 100644 index 000000000..1a2e10bb0 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedHeaderBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Header; + + public OpenApiHeader OldHeader { get; } + public OpenApiHeader NewHeader { get; } + private readonly DiffContextBO _context; + + public bool Required { get; set; } + public bool Deprecated { get; set; } + public bool Style { get; set; } + public bool Explode { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedSchemaBO Schema { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedHeaderBO(OpenApiHeader oldHeader, OpenApiHeader newHeader, DiffContextBO context) + { + OldHeader = oldHeader; + NewHeader = newHeader; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Description", Description), + ("Schema", Schema), + ("Content", Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!Required && !Deprecated && !Style && !Explode) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!Required && !Style && !Explode) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (Required) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", OldHeader?.Required.ToString(), NewHeader?.Required.ToString())); + + if (Deprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldHeader?.Deprecated.ToString(), NewHeader?.Deprecated.ToString())); + + if (Style) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Style", OldHeader?.Style.ToString(), NewHeader?.Style.ToString())); + + if (Explode) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Explode", OldHeader?.Explode.ToString(), NewHeader?.Explode.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs new file mode 100644 index 000000000..c9c7d1133 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedHeadersBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Header; + + private readonly IDictionary _oldHeaders; + private readonly IDictionary _newHeaders; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedHeadersBO(IDictionary oldHeaders, IDictionary newHeaders, DiffContextBO context) + { + _oldHeaders = oldHeaders; + _newHeaders = newHeaders; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs new file mode 100644 index 000000000..376ac17e7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedInfoBO + { + public ChangedElementTypeEnum ElementType { get; } + public TypeEnum ChangeType { get; } + public string FieldName { get; } + public string OldValue { get; } + public string NewValue { get; } + + public ChangedInfoBO(ChangedElementTypeEnum elementType, TypeEnum changeType, string fieldName, string oldValue, string newValue) + { + ElementType = elementType; + ChangeType = changeType; + FieldName = fieldName; + OldValue = oldValue; + NewValue = newValue; + } + + public static ChangedInfoBO ForAdded(ChangedElementTypeEnum elementType, string fieldName) + { + return new ChangedInfoBO(elementType, TypeEnum.Added, fieldName, null, null); + } + + public static ChangedInfoBO ForRemoved(ChangedElementTypeEnum elementType, string fieldName) + { + return new ChangedInfoBO(elementType, TypeEnum.Removed, fieldName, null, null); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs new file mode 100644 index 000000000..8ae8761ce --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedInfosBO + { + public List Path { get; set; } + public DiffResultBO ChangeType { get; set; } + public List Changes { get; set; } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs new file mode 100644 index 000000000..6ac77a5f4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ChangedListBO : ChangedBO + { + public readonly DiffContextBO Context; + public readonly IList OldValue; + public readonly IList NewValue; + + public List Increased { get; set; } + public List Missing { get; set; } + public List Shared { get; set; } + + protected ChangedListBO(IList oldValue, IList newValue, DiffContextBO context) + { + OldValue = oldValue; + NewValue = newValue; + Context = context; + Shared = new List(); + Increased = new List(); + Missing = new List(); + } + + public override DiffResultBO IsChanged() + { + if (Missing.IsNullOrEmpty() && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + return IsItemsChanged(); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + + foreach (var listElement in Increased) + { + returnList.Add(ChangedInfoBO.ForAdded(elementType, listElement.ToString())); + } + + foreach (var listElement in Missing) + { + returnList.Add(ChangedInfoBO.ForRemoved(elementType, listElement.ToString())); + } + return returnList; + } + + public abstract DiffResultBO IsItemsChanged(); + + //public class SimpleChangedList : ChangedListBO + //{ + // protected SimpleChangedList(List oldValue, List newValue) : base(oldValue, newValue, null) + // { + // } + + // public override DiffResultBO IsItemsChanged() + // { + // return new DiffResultBO(DiffResultEnum.Unknown); + // } + //} + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs new file mode 100644 index 000000000..83a885ed8 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMaxLengthBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.MaxLength; + + private readonly int? _oldValue; + private readonly int? _newValue; + private readonly DiffContextBO _context; + + public ChangedMaxLengthBO(int? oldValue, int? newValue, DiffContextBO context) + { + _oldValue = oldValue; + _newValue = newValue; + _context = context; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest && (_newValue == null || _oldValue != null && _oldValue <= _newValue) + || _context.IsResponse && (_oldValue == null || _newValue != null && _newValue <= _oldValue)) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue?.ToString(), _newValue?.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs new file mode 100644 index 000000000..4341fb3cf --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMediaTypeBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.MediaType; + + private readonly OpenApiSchema _oldSchema; + private readonly OpenApiSchema _newSchema; + private readonly DiffContextBO _context; + + public ChangedSchemaBO Schema { get; set; } + + public ChangedMediaTypeBO(OpenApiSchema oldSchema, OpenApiSchema newSchema, DiffContextBO context) + { + _oldSchema = oldSchema; + _newSchema = newSchema; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Schema), + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs new file mode 100644 index 000000000..c5bde4f3b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMetadataBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Metadata; + + public string Left { get; } + public string Right { get; } + + public ChangedMetadataBO(string left, string right) + { + Left = left; + Right = right; + } + + public override DiffResultBO IsChanged() + { + return Left == Right ? new DiffResultBO(DiffResultEnum.NoChanges) : new DiffResultBO(DiffResultEnum.Metadata); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (Left != Right) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Value", Left, Right)); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs new file mode 100644 index 000000000..73f85bbf3 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOAuthFlowBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.AuthFlow; + + public OpenApiOAuthFlow OldOAuthFlow { get; } + public OpenApiOAuthFlow NewOAuthFlow { get; } + + public bool ChangedAuthorizationUrl { get; set; } + public bool ChangedTokenUrl { get; set; } + public bool ChangedRefreshUrl { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOAuthFlowBO(OpenApiOAuthFlow oldOAuthFlow, OpenApiOAuthFlow newOAuthFlow) + { + OldOAuthFlow = oldOAuthFlow; + NewOAuthFlow = newOAuthFlow; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (ChangedAuthorizationUrl || ChangedTokenUrl || ChangedRefreshUrl) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (ChangedAuthorizationUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "AuthorizationUrl", OldOAuthFlow?.AuthorizationUrl.ToString(), NewOAuthFlow?.AuthorizationUrl.ToString())); + + if (ChangedTokenUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "TokenUrl", OldOAuthFlow?.TokenUrl.ToString(), NewOAuthFlow?.TokenUrl.ToString())); + + if (ChangedRefreshUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "RefreshUrl", OldOAuthFlow?.RefreshUrl.ToString(), NewOAuthFlow?.RefreshUrl.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs new file mode 100644 index 000000000..11365b04e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOAuthFlowsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.AuthFlow; + + private readonly OpenApiOAuthFlows _oldOAuthFlows; + private readonly OpenApiOAuthFlows _newOAuthFlows; + + public ChangedOAuthFlowBO ImplicitOAuthFlow { get; set; } + public ChangedOAuthFlowBO PasswordOAuthFlow { get; set; } + public ChangedOAuthFlowBO ClientCredentialOAuthFlow { get; set; } + public ChangedOAuthFlowBO AuthorizationCodeOAuthFlow { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOAuthFlowsBO(OpenApiOAuthFlows oldOAuthFlows, OpenApiOAuthFlows newOAuthFlows) + { + _oldOAuthFlows = oldOAuthFlows; + _newOAuthFlows = newOAuthFlows; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Implicit", ImplicitOAuthFlow), + ("Password", PasswordOAuthFlow), + ("ClientCredential", ClientCredentialOAuthFlow), + ("AuthorizationCode", AuthorizationCodeOAuthFlow), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs new file mode 100644 index 000000000..24d2ffedf --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOneOfSchemaBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.OneOf; + + private readonly Dictionary _oldMapping; + private readonly Dictionary _newMapping; + public DiffContextBO Context { get; } + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedOneOfSchemaBO( Dictionary oldMapping, Dictionary newMapping, DiffContextBO context) + { + _oldMapping = oldMapping; + _newMapping = newMapping; + Context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Context.IsRequest && Missing.IsNullOrEmpty() || Context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs new file mode 100644 index 000000000..a81ff5e48 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOpenApiBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.OpenApi; + public string OldSpecIdentifier { get; set; } + public string NewSpecIdentifier { get; set; } + public OpenApiDocument OldSpecOpenApi { get; set; } + public OpenApiDocument NewSpecOpenApi { get; set; } + public List NewEndpoints { get; set; } + public List MissingEndpoints { get; set; } + public List ChangedOperations { get; set; } + public ChangedExtensionsBO ChangedExtensions { get; set; } + + public ChangedOpenApiBO(string oldSpecIdentifier, string newSpecIdentifier) + { + NewEndpoints = new List(); + MissingEndpoints = new List(); + ChangedOperations = new List(); + OldSpecIdentifier = oldSpecIdentifier; + NewSpecIdentifier = newSpecIdentifier; + } + public List GetDeprecatedEndpoints() + { + return ChangedOperations + .Where(x => x.IsDeprecated) + .Select(x => x.ConvertToEndpoint()) + .ToList(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> ( + ChangedOperations.Select(x => (x.PathUrl, (ChangedBO)x)) + ) + { + (null, ChangedExtensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (NewEndpoints.IsNullOrEmpty() && MissingEndpoints.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (MissingEndpoints.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(NewEndpoints, MissingEndpoints, x => x.PathUrl); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs new file mode 100644 index 000000000..a81980b1c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOperationBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Operation; + + public OpenApiOperation OldOperation { get; } + public OpenApiOperation NewOperation { get; } + + public OperationType HttpMethod { get; } + public string PathUrl { get; } + public ChangedMetadataBO Summary { get; set; } + public ChangedMetadataBO Description { get; set; } + public bool IsDeprecated { get; set; } + public ChangedParametersBO Parameters { get; set; } + public ChangedRequestBodyBO RequestBody { get; set; } + public ChangedAPIResponseBO APIResponses { get; set; } + public ChangedSecurityRequirementsBO SecurityRequirements { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOperationBO(string pathUrl, OperationType httpMethod, OpenApiOperation oldOperation, OpenApiOperation newOperation) + { + PathUrl = pathUrl; + HttpMethod = httpMethod; + OldOperation = oldOperation; + NewOperation = newOperation; + } + + public EndpointBO ConvertToEndpoint() + { + var endpoint = new EndpointBO + { + PathUrl = PathUrl, + Method = HttpMethod, + Summary = NewOperation.Summary, + Operation = NewOperation + }; + return endpoint; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Summary", Summary), + ("Description", Description), + ("Parameters", Parameters), + ("RequestBody", RequestBody), + ("Responses", APIResponses), + ("SecurityRequirements", SecurityRequirements), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (IsDeprecated) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldOperation?.Deprecated.ToString(), NewOperation?.Deprecated.ToString())); + + return returnList; + } + + public DiffResultBO ResultApiResponses() + { + return Result(APIResponses); + } + + public DiffResultBO ResultRequestBody() + { + return RequestBody == null ? new DiffResultBO(DiffResultEnum.NoChanges) : RequestBody.IsChanged(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs new file mode 100644 index 000000000..dccc6fe2d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedParameterBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Parameter; + + private readonly DiffContextBO _context; + public ParameterLocation? In { get; set; } + public string Name { get; set; } + public OpenApiParameter OldParameter { get; } + public OpenApiParameter NewParameter { get; } + public bool IsChangeRequired { get; set; } + public bool IsDeprecated { get; set; } + public bool ChangeStyle { get; set; } + public bool ChangeExplode { get; set; } + public bool ChangeAllowEmptyValue { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedSchemaBO Schema { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedParameterBO(string name, ParameterLocation? @in, OpenApiParameter oldParameter, OpenApiParameter newParameter, DiffContextBO context) + { + _context = context; + Name = name; + In = @in; + OldParameter = oldParameter; + NewParameter = newParameter; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, Schema), + (null, Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!IsChangeRequired + && !IsDeprecated + && !ChangeAllowEmptyValue + && !ChangeStyle + && !ChangeExplode) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if ((!IsChangeRequired || OldParameter.Required) + && (!ChangeAllowEmptyValue || NewParameter.AllowEmptyValue) + && !ChangeStyle + && !ChangeExplode) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangeRequired) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", OldParameter?.Required.ToString(), NewParameter?.Required.ToString())); + + if (IsDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldParameter?.Deprecated.ToString(), NewParameter?.Deprecated.ToString())); + + if (ChangeStyle) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Style", OldParameter?.Style.ToString(), NewParameter?.Style.ToString())); + + if (ChangeExplode) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Explode", OldParameter?.Explode.ToString(), NewParameter?.Explode.ToString())); + + if (ChangeAllowEmptyValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, "AllowEmptyValue", OldParameter?.AllowEmptyValue.ToString(), NewParameter?.AllowEmptyValue.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs new file mode 100644 index 000000000..cca278d73 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedParametersBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Parameter; + + private readonly List _oldParameterList; + private readonly List _newParameterList; + private readonly DiffContextBO _context; + public List Increased { get; set; } + public List Missing { get; set; } + public List Changed { get; set; } + + public ChangedParametersBO(List oldParameterList, List newParameterList, DiffContextBO context) + { + _oldParameterList = oldParameterList; + _newParameterList = newParameterList; + _context = context; + Increased = new List(); + Missing = new List(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> ( + Changed.Select(x => (x.Name, (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + if (Increased.Any(x => x.Required) && Missing.IsNullOrEmpty()) + return new DiffResultBO(DiffResultEnum.Compatible); + + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased, Missing, x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs new file mode 100644 index 000000000..c154ebe52 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedPathBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Path; + + private readonly string _pathUrl; + private readonly OpenApiPathItem _oldPath; + private readonly OpenApiPathItem _newPath; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public List Changed { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedPathBO(string pathUrl, OpenApiPathItem oldPath, OpenApiPathItem newPath, DiffContextBO context) + { + _pathUrl = pathUrl; + _oldPath = oldPath; + _newPath = newPath; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.PathUrl, (ChangedBO)x)) + ) + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x.ToString()); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs new file mode 100644 index 000000000..a3af1f0ec --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedPathsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Path; + + private readonly Dictionary _oldPathMap; + private readonly Dictionary _newPathMap; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedPathsBO(Dictionary oldPathMap, Dictionary newPathMap) + { + _oldPathMap = oldPathMap; + _newPathMap = newPathMap; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.ToList(), Missing.ToList(), x => x.Key); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs new file mode 100644 index 000000000..b2a726d22 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedReadOnlyBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.ReadOnly; + + private readonly DiffContextBO _context; + private readonly bool _oldValue; + private readonly bool _newValue; + + public ChangedReadOnlyBO(bool? oldValue, bool? newValue, DiffContextBO context) + { + _context = context; + _oldValue = oldValue ?? false; + _newValue = newValue ?? false; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsResponse) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + if (_context.IsRequest) + { + if (_newValue) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + return _context.IsRequired ? new DiffResultBO(DiffResultEnum.Incompatible) : new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Unknown); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue.ToString(), _newValue.ToString())); + + return returnList; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs new file mode 100644 index 000000000..432b9f8e5 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedRequestBodyBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.RequestBody; + + private readonly OpenApiRequestBody _oldRequestBody; + private readonly OpenApiRequestBody _newRequestBody; + private readonly DiffContextBO _context; + + public bool ChangeRequired { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedRequestBodyBO(OpenApiRequestBody oldRequestBody, OpenApiRequestBody newRequestBody, DiffContextBO context) + { + _oldRequestBody = oldRequestBody; + _newRequestBody = newRequestBody; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Description", Description), + ("Content", Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!ChangeRequired) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldRequestBody?.Required != _newRequestBody?.Required) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", _oldRequestBody?.Required.ToString(), _newRequestBody?.Required.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs new file mode 100644 index 000000000..e044a301d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedRequiredBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Required; + + public ChangedRequiredBO(IList oldValue, IList newValue, DiffContextBO context) : base(oldValue, newValue, context) + { + } + + public override DiffResultBO IsItemsChanged() + { + if (Context.IsRequest && Increased.IsNullOrEmpty() + || Context.IsResponse && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs new file mode 100644 index 000000000..7c366fe27 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedResponseBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Response; + + private readonly DiffContextBO _context; + public OpenApiResponse OldApiResponse { get; } + public OpenApiResponse NewApiResponse { get; } + public ChangedMetadataBO Description { get; set; } + public ChangedHeadersBO Headers { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedResponseBO(OpenApiResponse oldApiResponse, OpenApiResponse newApiResponse, DiffContextBO context) + { + OldApiResponse = oldApiResponse; + NewApiResponse = newApiResponse; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, Headers), + (null, Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs new file mode 100644 index 000000000..87b25fb32 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSchemaBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Schema; + + public DiffContextBO Context { get; set; } + public OpenApiSchema OldSchema { get; set; } + public OpenApiSchema NewSchema { get; set; } + public string Type { get; set; } + public Dictionary ChangedProperties { get; set; } + public Dictionary IncreasedProperties { get; set; } + public Dictionary MissingProperties { get; set; } + public bool IsChangeDeprecated { get; set; } + public ChangedMetadataBO Description { get; set; } + public bool IsChangeTitle { get; set; } + public ChangedRequiredBO Required { get; set; } + public bool IsChangeDefault { get; set; } + public ChangedEnumBO Enumeration { get; set; } + public bool IsChangeFormat { get; set; } + public ChangedReadOnlyBO ReadOnly { get; set; } + public ChangedWriteOnlyBO WriteOnly { get; set; } + public bool IsChangedType { get; set; } + public ChangedMaxLengthBO MaxLength { get; set; } + public bool DiscriminatorPropertyChanged { get; set; } + public ChangedSchemaBO Items { get; set; } + public ChangedOneOfSchemaBO OneOfSchema { get; set; } + public ChangedSchemaBO AddProp { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedSchemaBO() + { + IncreasedProperties = new Dictionary(); + MissingProperties = new Dictionary(); + ChangedProperties = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + ChangedProperties.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + { + ("Description", Description), + ("ReadOnly", ReadOnly), + ("WriteOnly", WriteOnly), + ("Items", Items), + ("OneOfSchema", OneOfSchema), + ("AddProp", AddProp), + ("Enumeration", Enumeration), + ("Required", Required), + ("MaxLength", MaxLength), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if ( + !IsChangedType + && (OldSchema == null && NewSchema == null || OldSchema != null && NewSchema != null) + && !IsChangeFormat + && IncreasedProperties.Count == 0 + && MissingProperties.Count == 0 + && ChangedProperties.Values.Count == 0 + && !IsChangeDeprecated + && ! DiscriminatorPropertyChanged + ) + return new DiffResultBO(DiffResultEnum.NoChanges); + + var compatibleForRequest = OldSchema != null || NewSchema == null; + var compatibleForResponse = + MissingProperties.IsNullOrEmpty() && (OldSchema == null || NewSchema != null); + + if ((Context.IsRequest && compatibleForRequest + || Context.IsResponse && compatibleForResponse) + && !IsChangedType + && !DiscriminatorPropertyChanged) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = GetCoreChangeInfosOfComposed(IncreasedProperties.Keys.ToList(), MissingProperties.Keys.ToList(), x => x); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangedType) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Type", OldSchema?.Type, NewSchema?.Type)); + + if (IsChangeDefault) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Default", OldSchema?.Default.ToString(), NewSchema?.Default.ToString())); + + if (IsChangeDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldSchema?.Deprecated.ToString(), NewSchema?.Deprecated.ToString())); + + if (IsChangeFormat) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Format", OldSchema?.Format, NewSchema?.Format)); + + if (IsChangeTitle) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Title", OldSchema?.Title, NewSchema?.Title)); + + if (DiscriminatorPropertyChanged) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Discriminator Property", OldSchema?.Discriminator?.PropertyName, NewSchema?.Discriminator?.PropertyName)); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs new file mode 100644 index 000000000..b7181d5ea --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecurityRequirementBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityRequirement; + + private readonly OpenApiSecurityRequirement _oldSecurityRequirement; + private readonly OpenApiSecurityRequirement _newSecurityRequirement; + + public OpenApiSecurityRequirement Missing { get; set; } + public OpenApiSecurityRequirement Increased { get; set; } + public List Changed { get; set; } + + public ChangedSecurityRequirementBO(OpenApiSecurityRequirement newSecurityRequirement, OpenApiSecurityRequirement oldSecurityRequirement) + { + _newSecurityRequirement = newSecurityRequirement; + _oldSecurityRequirement = oldSecurityRequirement; + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.NewSecurityScheme.Name ?? x.OldSecurityScheme.Name, (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased == null && Missing == null) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Increased == null) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs new file mode 100644 index 000000000..35509be31 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecurityRequirementsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityRequirement; + + private readonly IList _oldSecurityRequirements; + private readonly IList _newSecurityRequirements; + + public List Missing { get; set; } + public List Increased { get; set; } + public List Changed { get; set; } + + public ChangedSecurityRequirementsBO(IList oldSecurityRequirements, IList newSecurityRequirements) + { + _oldSecurityRequirements = oldSecurityRequirements; + _newSecurityRequirements = newSecurityRequirements; + Missing = new List(); + Increased = new List(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (GetElementType().GetDisplayName(), (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Missing.IsNullOrEmpty() && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.SelectMany(x => x.Keys).ToList(), Missing.SelectMany(x => x.Keys).ToList(), x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs new file mode 100644 index 000000000..103193846 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecuritySchemeBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityScheme; + + public OpenApiSecurityScheme OldSecurityScheme { get; } + public OpenApiSecurityScheme NewSecurityScheme { get; } + + public bool IsChangedType { get; set; } + public bool IsChangedIn { get; set; } + public bool IsChangedScheme { get; set; } + public bool IsChangedBearerFormat { get; set; } + public bool IsChangedOpenIdConnectUrl { get; set; } + public ChangedSecuritySchemeScopesBO ChangedScopes { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedOAuthFlowsBO OAuthFlows { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedSecuritySchemeBO(OpenApiSecurityScheme oldSecurityScheme, OpenApiSecurityScheme newSecurityScheme) + { + OldSecurityScheme = oldSecurityScheme; + NewSecurityScheme = newSecurityScheme; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, OAuthFlows), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!IsChangedType + && !IsChangedIn + && !IsChangedScheme + && !IsChangedBearerFormat + && !IsChangedOpenIdConnectUrl + && (ChangedScopes == null || ChangedScopes.IsUnchanged())) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!IsChangedType + && !IsChangedIn + && !IsChangedScheme + && !IsChangedBearerFormat + && !IsChangedOpenIdConnectUrl + && (ChangedScopes == null || ChangedScopes.Increased.IsNullOrEmpty())) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangedBearerFormat) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Bearer Format", OldSecurityScheme?.BearerFormat, NewSecurityScheme?.BearerFormat)); + + if (IsChangedIn) + returnList.Add(new ChangedInfoBO(elementType, changeType, "In", OldSecurityScheme?.In.ToString(), NewSecurityScheme?.In.ToString())); + + if (IsChangedOpenIdConnectUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "OpenIdConnect Url", OldSecurityScheme?.OpenIdConnectUrl.ToString(), NewSecurityScheme?.OpenIdConnectUrl.ToString())); + + if (IsChangedScheme) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Scheme", OldSecurityScheme?.Scheme, NewSecurityScheme?.Scheme)); + + if (IsChangedType) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Type", OldSecurityScheme?.Type.ToString(), NewSecurityScheme?.Type.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs new file mode 100644 index 000000000..33ee349d4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecuritySchemeScopesBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecuritySchemeScope; + + public ChangedSecuritySchemeScopesBO(List oldValue, List newValue) : base(oldValue, newValue, null) + { + } + + public override DiffResultBO IsItemsChanged() + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs new file mode 100644 index 000000000..db0613fe9 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedWriteOnlyBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.WriteOnly; + + private readonly DiffContextBO _context; + private readonly bool _oldValue; + private readonly bool _newValue; + + public ChangedWriteOnlyBO(bool? oldValue, bool? newValue, DiffContextBO context) + { + _context = context; + _oldValue = oldValue ?? false; + _newValue = newValue ?? false; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + if (_context.IsResponse) + { + if (_newValue) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + return _context.IsRequired ? new DiffResultBO(DiffResultEnum.Incompatible) : new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Unknown); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if(_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue.ToString(), _newValue.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs new file mode 100644 index 000000000..5ac556bb7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ComposedChangedBO : ChangedBO + { + protected ComposedChangedBO() + { + } + + public abstract List<(string Identifier, ChangedBO Change)> GetChangedElements(); + + public override List GetAllChangeInfoFlat(string identifier, List parentPath = null) + { + var coreChangeInfo = GetCoreChangeInfo(identifier, parentPath); + var changedElements = GetChangedElements(); + var returnList = changedElements + .SelectMany(x => x.Change.GetAllChangeInfoFlat(x.Identifier, coreChangeInfo.Path)) + .Where(x => !x.ChangeType.IsUnchanged()) + .OrderBy(x => x.Path.Count) + .ToList(); + + returnList.Add(coreChangeInfo); + + return returnList; + } + + public override DiffResultBO IsChanged() + { + var elementsResultMax = GetChangedElements() + .Where(x => x.Change != null) + .Select(x => (int)x.Change.IsChanged().DiffResult) + .DefaultIfEmpty(0) + .Max(); + + var elementsResult = new DiffResultBO((DiffResultEnum)elementsResultMax); + + return IsCoreChanged().DiffResult > elementsResult.DiffResult ? IsCoreChanged() : elementsResult; + } + + protected List GetCoreChangeInfosOfComposed(List increased, List missing, Func identifierSelector) + { + var returnList = new List(); + var elementType = GetElementType(); + + foreach (var listElement in increased) + { + returnList.Add(ChangedInfoBO.ForAdded(elementType, identifierSelector(listElement))); + } + + foreach (var listElement in missing) + { + returnList.Add(ChangedInfoBO.ForRemoved(elementType, identifierSelector(listElement))); + } + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs new file mode 100644 index 000000000..b0888f391 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class DiffContextBO + { + public string URL { get; set; } + public Dictionary Parameters { get; set; } + public OperationType Method { get; private set; } + public bool IsResponse { get; private set; } + public bool IsRequest { get; private set; } + + public bool IsRequired { get; private set; } + + public DiffContextBO() + { + Parameters = new Dictionary(); + IsResponse = false; + IsRequest = true; + } + + public string GetDiffContextElementType() => IsResponse ? "Response" : "Request"; + + + public DiffContextBO CopyWithMethod(OperationType method) + { + var result = Copy(); + result.Method = method; + return result; + } + + public DiffContextBO CopyWithRequired(bool required) + { + var result = Copy(); + result.IsRequired = required; + return result; + } + + public DiffContextBO CopyAsRequest() + { + var result = Copy(); + result.IsRequest = true; + result.IsResponse = false; + return result; + } + + public DiffContextBO CopyAsResponse() + { + var result = Copy(); + result.IsResponse = true; + result.IsRequest = false; + return result; + } + + private DiffContextBO Copy() + { + var context = new DiffContextBO + { + URL = URL, + Parameters = Parameters, + Method = Method, + IsResponse = IsResponse, + IsRequest = IsRequest, + IsRequired = IsRequired + }; + return context; + } + public override bool Equals(object o) + { + if (this == o) return true; + + if (o == null || GetType() != o.GetType()) return false; + + var that = (DiffContextBO)o; + + return IsResponse.Equals(that.IsResponse) + && IsRequest.Equals(that.IsRequest) + && URL.Equals(that.URL) + && Parameters.Equals(that.Parameters) + && Method.Equals(that.Method) + && IsRequired.Equals(that.IsRequired); + } + + public override int GetHashCode() + { + return HashCode.Combine(URL, Parameters, Method, IsResponse, IsRequest, IsRequired); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs new file mode 100644 index 000000000..c1b6b9b7c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs @@ -0,0 +1,39 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class DiffResultBO + { + public readonly DiffResultEnum DiffResult; + + public DiffResultBO(DiffResultEnum diffResult) + { + DiffResult = diffResult; + } + + public bool IsUnchanged() + { + return DiffResult == 0; + } + + public bool IsDifferent() + { + return DiffResult > 0; + } + + public bool IsIncompatible() + { + return (int)DiffResult > 2; + } + + public bool IsCompatible() + { + return (int)DiffResult <= 2; + } + + public bool IsMetaChanged() + { + return (int)DiffResult == 1; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs new file mode 100644 index 000000000..ba96adf0f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs @@ -0,0 +1,18 @@ +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class EndpointBO + { + public string PathUrl { get; set; } + public OperationType Method { get; set; } + public string Summary { get; set; } + public OpenApiPathItem Path { get; set; } + public OpenApiOperation Operation { get; set; } + + public override string ToString() + { + return Method + " " + PathUrl; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs new file mode 100644 index 000000000..c47104e78 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ApiResponseDiff + { + private readonly OpenApiDiff _openApiDiff; + + public ApiResponseDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedAPIResponseBO Diff(OpenApiResponses left, OpenApiResponses right, DiffContextBO context) + { + var responseMapKeyDiff = MapKeyDiff.Diff(left, right); + var sharedResponseCodes = responseMapKeyDiff.SharedKey; + var responses = new Dictionary(); + foreach (var responseCode in sharedResponseCodes) + { + var diff = _openApiDiff + .ResponseDiff + .Diff(left[responseCode], right[responseCode], context); + + if(diff!= null) + responses.Add(responseCode, diff); + } + + var changedApiResponse = + new ChangedAPIResponseBO(left, right, context) + { + Increased = responseMapKeyDiff.Increased, + Missing = responseMapKeyDiff.Missing, + Changed = responses, + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedApiResponse); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs b/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs new file mode 100644 index 000000000..5df74dd47 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class CacheKey : IEquatable + { + private readonly string left; + private readonly string right; + private readonly DiffContextBO context; + + public CacheKey(string left, string right, DiffContextBO context) + { + this.left = left; + this.right = right; + this.context = context; + } + + public override bool Equals(object obj) + { + if (this == obj) return true; + + if (obj == null || GetType() != obj.GetType()) return false; + + var cacheKey = (CacheKey)obj; + + return Equals(cacheKey); + } + + public bool Equals(CacheKey other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return left == other.left && right == other.right && Equals(context, other.context); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (left != null ? left.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (right != null ? right.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (context != null ? context.GetHashCode() : 0); + return hashCode; + } + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs new file mode 100644 index 000000000..b2f698f53 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ContentDiff : IEquatable> + { + private readonly OpenApiDiff _openApiDiff; + + public ContentDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public bool Equals(IDictionary other) + { + return false; + } + + public ChangedContentBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + var leftDict = (Dictionary)left; + var rightDict = (Dictionary)right; + + + var mediaTypeDiff = MapKeyDiff.Diff(leftDict, rightDict); + var sharedMediaTypes = mediaTypeDiff.SharedKey; + var changedMediaTypes = new Dictionary(); + foreach (var sharedMediaType in sharedMediaTypes) + { + var oldMediaType = left[sharedMediaType]; + var newMediaType = right[sharedMediaType]; + var changedMediaType = + new ChangedMediaTypeBO(oldMediaType?.Schema, newMediaType?.Schema, context) + { + Schema = _openApiDiff + .SchemaDiff + .Diff( + new HashSet(), + oldMediaType?.Schema, + newMediaType?.Schema, + context.CopyWithRequired(true)) + }; + if (!ChangedUtils.IsUnchanged(changedMediaType)) + { + changedMediaTypes.Add(sharedMediaType, changedMediaType); + } + } + + return ChangedUtils.IsChanged(new ChangedContentBO(leftDict, rightDict, context) + { + Increased = mediaTypeDiff.Increased, + Missing = mediaTypeDiff.Missing, + Changed = changedMediaTypes + }); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs new file mode 100644 index 000000000..e1b65d5ea --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs @@ -0,0 +1,20 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public abstract class ExtensionDiff : IExtensionDiff + { + public abstract ExtensionDiff SetOpenApiDiff(OpenApiDiff openApiDiff); + + public abstract string GetName(); + + public abstract ChangedBO Diff(ChangeBO extension, DiffContextBO context) + where T : class; + + public virtual bool IsParentApplicable(TypeEnum type, object objectElement, object extension, DiffContextBO context) + { + return true; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs new file mode 100644 index 000000000..8f434e117 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ExtensionsDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly IEnumerable _extensions; + + public ExtensionsDiff(OpenApiDiff openApiDiff, IEnumerable extensions) + { + _openApiDiff = openApiDiff; + _extensions = extensions; + } + + public bool IsParentApplicable(TypeEnum type, object parent, IDictionary extensions, DiffContextBO context) + { + if (extensions.IsNullOrEmpty()) + return true; + + return extensions.Select(x => ExecuteExtension(x.Key, y => y + .IsParentApplicable(type, parent, x.Value, context))) + .All(x => x); + } + + public IExtensionDiff GetExtensionDiff(string name) + { + if (_extensions.IsNullOrEmpty()) + return null; + + return _extensions.FirstOrDefault(x => $"x-{x.GetName()}" == name); + } + + public T ExecuteExtension(string name, Func predicate) + { + if (_extensions.IsNullOrEmpty()) + return default; + + return predicate(GetExtensionDiff(name).SetOpenApiDiff(_openApiDiff)); + } + + public ChangedExtensionsBO Diff(IDictionary left, IDictionary right) + { + return Diff(left, right, null); + } + + public ChangedExtensionsBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + left = ((Dictionary)left).CopyDictionary(); + right = ((Dictionary)right).CopyDictionary(); + var changedExtensions = new ChangedExtensionsBO((Dictionary)left, ((Dictionary)right).CopyDictionary(), context); + foreach (var (key, value) in left) + { + if (right.ContainsKey(key)) + { + var rightValue = right[key]; + right.Remove(key); + var changed = ExecuteExtensionDiff(key, ChangeBO.Changed(value, rightValue), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Changed.Add(key, changed); + } + else + { + var changed = ExecuteExtensionDiff(key, ChangeBO.Removed(value), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Missing.Add(key, changed); + } + } + + foreach (var (key, value) in right) + { + var changed = ExecuteExtensionDiff(key, ChangeBO.Added(value), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Increased.Add(key, changed); + } + + return ChangedUtils.IsChanged(changedExtensions); + } + + private ChangedBO ExecuteExtensionDiff(string name, ChangeBO change, DiffContextBO context) + where T : class + { + return ExecuteExtension(name, x => x.Diff(change, context)); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs new file mode 100644 index 000000000..63f0afcb2 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class HeaderDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Headers); + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public HeaderDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedHeaderBO Diff(OpenApiHeader left, OpenApiHeader right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedHeaderBO ComputeDiff(HashSet refSet, OpenApiHeader left, OpenApiHeader right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedHeader = + new ChangedHeaderBO(left, right, context) + { + Required = GetBooleanDiff(left.Required, right.Required), + Deprecated = !left.Deprecated && right.Deprecated, + Style = left.Style != right.Style, + Explode = GetBooleanDiff(left.Explode, right.Explode), + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Schema = _openApiDiff + .SchemaDiff + .Diff(new HashSet(), left.Schema, right.Schema, context.CopyWithRequired(true)), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedHeader); + } + + private static bool GetBooleanDiff(bool? left, bool? right) + { + var leftRequired = left ?? false; + var rightRequired = right ?? false; + return leftRequired != rightRequired; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs new file mode 100644 index 000000000..58ce9113c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class HeadersDiff + { + private readonly OpenApiDiff _openApiDiff; + + public HeadersDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedHeadersBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + var headerMapDiff = MapKeyDiff.Diff(left, right); + var sharedHeaderKeys = headerMapDiff.SharedKey; + + var changed = new Dictionary(); + foreach (var headerKey in sharedHeaderKeys) + { + var oldHeader = left[headerKey]; + var newHeader = right[headerKey]; + var changedHeaders = _openApiDiff + .HeaderDiff + .Diff(oldHeader, newHeader, context); + if (changedHeaders != null) + changed.Add(headerKey, changedHeaders); + } + + return ChangedUtils.IsChanged( + new ChangedHeadersBO(left, right, context) + { + Increased = headerMapDiff.Increased, + Missing = headerMapDiff.Missing, + Changed = changed + }); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs new file mode 100644 index 000000000..66ace7e55 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public interface IExtensionDiff + { + ExtensionDiff SetOpenApiDiff(OpenApiDiff openApiDiff); + + string GetName(); + + ChangedBO Diff(ChangeBO extension, DiffContextBO context) + where T : class; + + bool IsParentApplicable(TypeEnum type, object objectElement, object extension, DiffContextBO context); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs new file mode 100644 index 000000000..faec10f06 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs @@ -0,0 +1,42 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public static class ListDiff + { + public static T1 Diff(T1 instance) + where T1 : ChangedListBO + { + if (instance.OldValue.IsNullOrEmpty() && instance.NewValue.IsNullOrEmpty()) + { + return instance; + } + if (instance.OldValue.IsNullOrEmpty()) + { + instance.Increased = instance.NewValue.ToList(); + return instance; + } + if (instance.NewValue.IsNullOrEmpty()) + { + instance.Missing = instance.OldValue.ToList(); + return instance; + } + instance.Increased.AddRange(instance.NewValue); + foreach (var leftItem in instance.OldValue) + { + if (instance.NewValue.Contains(leftItem)) + { + instance.Increased.Remove(leftItem); + instance.Shared.Add(leftItem); + } + else + { + instance.Missing.Add(leftItem); + } + } + return instance; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs new file mode 100644 index 000000000..5d8238122 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class MapKeyDiff + { + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public List SharedKey { get; set; } + + private MapKeyDiff() + { + SharedKey = new List(); + Increased = new Dictionary(); + Missing = new Dictionary(); + } + + public static MapKeyDiff Diff(IDictionary mapLeft, IDictionary mapRight) + { + var instance = new MapKeyDiff(); + if (null == mapLeft && null == mapRight) return instance; + if (null == mapLeft) + { + instance.Increased = (Dictionary)mapRight; + return instance; + } + if (null == mapRight) + { + instance.Missing = (Dictionary)mapLeft; + return instance; + } + instance.Increased = new Dictionary(mapRight); + instance.Missing = new Dictionary(); + foreach (var entry in mapLeft) + { + var leftKey = entry.Key; + var leftValue = entry.Value; + if (mapRight.ContainsKey(leftKey)) + { + instance.Increased.Remove(leftKey); + instance.SharedKey.Add(leftKey); + } + else + { + instance.Missing.Add(leftKey, leftValue); + } + } + return instance; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs new file mode 100644 index 000000000..f3bbb3929 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs @@ -0,0 +1,25 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class MetadataDiff + { + private OpenApiComponents _leftComponents; + private OpenApiComponents _rightComponents; + private OpenApiDiff _openApiDiff; + + public MetadataDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedMetadataBO Diff(string left, string right, DiffContextBO context) + { + return ChangedUtils.IsChanged(new ChangedMetadataBO(left, right)); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs new file mode 100644 index 000000000..7e6177206 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OAuthFlowDiff + { + private readonly OpenApiDiff _openApiDiff; + public OAuthFlowDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOAuthFlowBO Diff(OpenApiOAuthFlow left, OpenApiOAuthFlow right) + { + var changedOAuthFlow = new ChangedOAuthFlowBO(left, right); + if (left != null && right != null) + { + changedOAuthFlow.ChangedAuthorizationUrl = left.AuthorizationUrl != right.AuthorizationUrl; + changedOAuthFlow.ChangedTokenUrl = left.TokenUrl != right.TokenUrl; + changedOAuthFlow.ChangedRefreshUrl = left.RefreshUrl != right.RefreshUrl; + } + + changedOAuthFlow.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left?.Extensions, right?.Extensions); + + return ChangedUtils.IsChanged(changedOAuthFlow); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs new file mode 100644 index 000000000..ac895bcfa --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs @@ -0,0 +1,41 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OAuthFlowsDiff + { + private readonly OpenApiDiff _openApiDiff; + public OAuthFlowsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOAuthFlowsBO Diff(OpenApiOAuthFlows left, OpenApiOAuthFlows right) + { + var changedOAuthFlows = new ChangedOAuthFlowsBO(left, right); + if (left != null && right != null) + { + changedOAuthFlows.ImplicitOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.Implicit, right.Implicit); + changedOAuthFlows.PasswordOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.Password, right.Password); + changedOAuthFlows.ClientCredentialOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.ClientCredentials, right.ClientCredentials); + changedOAuthFlows.AuthorizationCodeOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.AuthorizationCode, right.AuthorizationCode); + } + + changedOAuthFlows.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left?.Extensions, right?.Extensions); + + return ChangedUtils.IsChanged(changedOAuthFlows); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs new file mode 100644 index 000000000..38bd3378a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OpenApiDiff + { + private readonly ILogger _logger; + + public string OldIdentifier { get; } + public string NewIdentifier { get; } + + public PathsDiff PathsDiff { get; set; } + public PathDiff PathDiff { get; set; } + public SchemaDiff SchemaDiff { get; set; } + public ContentDiff ContentDiff { get; set; } + public ParametersDiff ParametersDiff { get; set; } + public ParameterDiff ParameterDiff { get; set; } + public RequestBodyDiff RequestBodyDiff { get; set; } + public ResponseDiff ResponseDiff { get; set; } + public HeadersDiff HeadersDiff { get; set; } + public HeaderDiff HeaderDiff { get; set; } + public ApiResponseDiff APIResponseDiff { get; set; } + public OperationDiff OperationDiff { get; set; } + public SecurityRequirementsDiff SecurityRequirementsDiff { get; set; } + public SecurityRequirementDiff SecurityRequirementDiff { get; set; } + public SecuritySchemeDiff SecuritySchemeDiff { get; set; } + public OAuthFlowsDiff OAuthFlowsDiff { get; set; } + public OAuthFlowDiff OAuthFlowDiff { get; set; } + public ExtensionsDiff ExtensionsDiff { get; set; } + public MetadataDiff MetadataDiff { get; set; } + public OpenApiDocument OldSpecOpenApi { get; set; } + public OpenApiDocument NewSpecOpenApi { get; set; } + public List NewEndpoints { get; set; } + public List MissingEndpoints { get; set; } + public List ChangedOperations { get; set; } + public ChangedExtensionsBO ChangedExtensions { get; set; } + + public OpenApiDiff(OpenApiDocument oldSpecOpenApi, string oldSpecIdentifier, OpenApiDocument newSpecOpenApi, string newSpecIdentifier, IEnumerable extensions, ILogger logger) + { + _logger = logger; + OldSpecOpenApi = oldSpecOpenApi; + NewSpecOpenApi = newSpecOpenApi; + OldIdentifier = oldSpecIdentifier; + NewIdentifier = newSpecIdentifier; + + if (null == oldSpecOpenApi || null == newSpecOpenApi) + throw new Exception("one of the old or new object is null"); + + InitializeFields(extensions); + } + + public static ChangedOpenApiBO Compare(OpenApiDocument oldSpecOpenApi, string oldSpecIdentifier, OpenApiDocument newSpecOpenApi, string newSpecIdentifier, IEnumerable extensions, ILogger logger) + { + return new OpenApiDiff(oldSpecOpenApi, oldSpecIdentifier, newSpecOpenApi, newSpecIdentifier, extensions, logger).Compare(); + } + + private void InitializeFields(IEnumerable extensions) + { + PathsDiff = new PathsDiff(this); + PathDiff = new PathDiff(this); + SchemaDiff = new SchemaDiff(this); + ContentDiff = new ContentDiff(this); + ParametersDiff = new ParametersDiff(this); + ParameterDiff = new ParameterDiff(this); + RequestBodyDiff = new RequestBodyDiff(this); + ResponseDiff = new ResponseDiff(this); + HeadersDiff = new HeadersDiff(this); + HeaderDiff = new HeaderDiff(this); + APIResponseDiff = new ApiResponseDiff(this); + OperationDiff = new OperationDiff(this); + SecurityRequirementsDiff = new SecurityRequirementsDiff(this); + SecurityRequirementDiff = new SecurityRequirementDiff(this); + SecuritySchemeDiff = new SecuritySchemeDiff(this); + OAuthFlowsDiff = new OAuthFlowsDiff(this); + OAuthFlowDiff = new OAuthFlowDiff(this); + ExtensionsDiff = new ExtensionsDiff(this, extensions); + MetadataDiff = new MetadataDiff(this); + } + + private ChangedOpenApiBO Compare() + { + PreProcess(OldSpecOpenApi); + PreProcess(NewSpecOpenApi); + var paths = + PathsDiff.Diff(PathsDiff.ValOrEmpty(OldSpecOpenApi.Paths), PathsDiff.ValOrEmpty(NewSpecOpenApi.Paths)); + NewEndpoints = new List(); + MissingEndpoints = new List(); + ChangedOperations = new List(); + + if (paths != null) + { + NewEndpoints = EndpointUtils.ConvertToEndpointList(paths.Increased); + MissingEndpoints = EndpointUtils.ConvertToEndpointList(paths.Missing); + foreach (var (key, value) in paths.Changed) + { + NewEndpoints.AddRange(EndpointUtils.ConvertToEndpoints(key, value.Increased)); + MissingEndpoints.AddRange(EndpointUtils.ConvertToEndpoints(key, value.Missing)); + ChangedOperations.AddRange(value.Changed); + } + } + + var diff = ExtensionsDiff + .Diff(OldSpecOpenApi.Extensions, NewSpecOpenApi.Extensions); + + if (diff != null) + ChangedExtensions = diff; + return GetChangedOpenApi(); + } + + private static void PreProcess(OpenApiDocument openApi) + { + var securityRequirements = openApi.SecurityRequirements; + + if (securityRequirements != null) + { + var distinctSecurityRequirements = + securityRequirements.Distinct().ToList(); + var paths = openApi.Paths; + if (paths != null) + { + foreach (var openApiPathItem in paths.Values) + { + var operationsWithSecurity = openApiPathItem + .Operations + .Values + .Where(x => !x.Security.IsNullOrEmpty()); + foreach (var openApiOperation in operationsWithSecurity) + { + openApiOperation.Security = openApiOperation.Security.Distinct().ToList(); + } + var operationsWithoutSecurity = openApiPathItem + .Operations + .Values + .Where(x => x.Security.IsNullOrEmpty()); + foreach (var openApiOperation in operationsWithoutSecurity) + { + openApiOperation.Security = distinctSecurityRequirements; + } + } + } + + openApi.SecurityRequirements = null; + } + } + + private ChangedOpenApiBO GetChangedOpenApi() + { + return new ChangedOpenApiBO(OldIdentifier, NewIdentifier) + { + MissingEndpoints = MissingEndpoints, + NewEndpoints = NewEndpoints, + NewSpecOpenApi = NewSpecOpenApi, + OldSpecOpenApi = OldSpecOpenApi, + ChangedOperations = ChangedOperations, + ChangedExtensions = ChangedExtensions + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs new file mode 100644 index 000000000..6c6cd4d4b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OperationDiff + { + private readonly OpenApiDiff _openApiDiff; + + public OperationDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOperationBO Diff( + OpenApiOperation oldOperation, OpenApiOperation newOperation, DiffContextBO context) + { + var changedOperation = + new ChangedOperationBO(context.URL, context.Method, oldOperation, newOperation) + { + Summary = _openApiDiff + .MetadataDiff + .Diff(oldOperation.Summary, newOperation.Summary, context), + Description = _openApiDiff + .MetadataDiff + .Diff(oldOperation.Description, newOperation.Description, context), + IsDeprecated = !oldOperation.Deprecated && newOperation.Deprecated + }; + + if (oldOperation.RequestBody != null || newOperation.RequestBody != null) + changedOperation.RequestBody = _openApiDiff + .RequestBodyDiff + .Diff( + oldOperation.RequestBody, newOperation.RequestBody, context.CopyAsRequest()); + + var parametersDiff = _openApiDiff + .ParametersDiff + .Diff(oldOperation.Parameters.ToList(), newOperation.Parameters.ToList(), context); + + if (parametersDiff != null) + { + RemovePathParameters(context.Parameters, parametersDiff); + changedOperation.Parameters = parametersDiff; + } + + + if (oldOperation.Responses != null || newOperation.Responses != null) + { + + var diff = _openApiDiff + .APIResponseDiff + .Diff(oldOperation.Responses, newOperation.Responses, context.CopyAsResponse()); + + if (diff != null) + changedOperation.APIResponses = diff; + } + + if (oldOperation.Security != null || newOperation.Security != null) + { + var diff = _openApiDiff + .SecurityRequirementsDiff + .Diff(oldOperation.Security, newOperation.Security, context); + + if (diff != null) + changedOperation.SecurityRequirements = diff; + } + + changedOperation.Extensions = + _openApiDiff + .ExtensionsDiff + .Diff(oldOperation.Extensions, newOperation.Extensions, context); + + return ChangedUtils.IsChanged(changedOperation); + } + + public void RemovePathParameters(Dictionary pathParameters, ChangedParametersBO parameters) + { + foreach (var (oldParam, newParam) in pathParameters) + { + RemovePathParameter(oldParam, parameters.Missing); + RemovePathParameter(newParam, parameters.Increased); + } + } + + public void RemovePathParameter(string name, List parameters) + { + var openApiParameters = parameters + .FirstOrDefault(x => x.In == ParameterLocation.Path && x.Name == name); + if (!parameters.IsNullOrEmpty() && openApiParameters != null) + parameters.Remove(openApiParameters); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs new file mode 100644 index 000000000..186df4688 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ParameterDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Parameters); + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + + public ParameterDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedParameterBO Diff(OpenApiParameter left, OpenApiParameter right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedParameterBO ComputeDiff(HashSet refSet, OpenApiParameter left, OpenApiParameter right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedParameter = + new ChangedParameterBO(right.Name, right.In, left, right, context) + { + IsChangeRequired = GetBooleanDiff(left.Required, right.Required), + IsDeprecated = !left.Deprecated && right.Deprecated, + ChangeAllowEmptyValue = GetBooleanDiff(left.AllowEmptyValue, right.AllowEmptyValue), + ChangeStyle = left.Style != right.Style, + ChangeExplode = GetBooleanDiff(left.Explode, right.Explode), + Schema = _openApiDiff + .SchemaDiff + .Diff(refSet, left.Schema, right.Schema, context.CopyWithRequired(true)), + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedParameter); + } + + private static bool GetBooleanDiff(bool? left, bool? right) + { + var leftRequired = left ?? false; + var rightRequired = right ?? false; + return leftRequired != rightRequired; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs new file mode 100644 index 000000000..308f880a8 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ParametersDiff + { + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Parameters); + + public ParametersDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public static OpenApiParameter Contains(OpenApiComponents components, List parameters, OpenApiParameter parameter) + { + return parameters + .FirstOrDefault(x => + Same(RefPointer.ResolveRef(components, x, x.Reference?.ReferenceV3), parameter)); + } + + public static bool Same(OpenApiParameter left, OpenApiParameter right) + { + return left.Name == right.Name && left.In.Equals(right.In); + } + + public ChangedParametersBO Diff( + List left, List right, DiffContextBO context) + { + var changedParameters = + new ChangedParametersBO(left, right, context); + if (null == left) left = new List(); + if (null == right) right = new List(); + + foreach (var openApiParameter in left) + { + var leftPara = openApiParameter; + leftPara = RefPointer.ResolveRef(_leftComponents, leftPara, leftPara.Reference?.ReferenceV3); + + var rightParam = Contains(_rightComponents, right, leftPara); + if (rightParam == null) + { + changedParameters.Missing.Add(leftPara); + } + else + { + right.Remove(rightParam); + + var diff = _openApiDiff.ParameterDiff + .Diff(leftPara, rightParam, context); + if (diff != null) + changedParameters.Changed.Add(diff); + } + } + + changedParameters.Increased.AddRange(right); + + return ChangedUtils.IsChanged(changedParameters); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs new file mode 100644 index 000000000..b1d27920f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs @@ -0,0 +1,48 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class PathDiff + { + private readonly OpenApiDiff _openApiDiff; + + public PathDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedPathBO Diff(OpenApiPathItem left, OpenApiPathItem right, DiffContextBO context) + { + var oldOperationMap = left.Operations; + var newOperationMap = right.Operations; + var operationsDiff = + MapKeyDiff.Diff(oldOperationMap, newOperationMap); + var sharedMethods = operationsDiff.SharedKey; + var changedPath = new ChangedPathBO(context.URL, left, right, context) + { + Increased = operationsDiff.Increased, + Missing = operationsDiff.Missing + }; + foreach (var operationType in sharedMethods) + { + var oldOperation = oldOperationMap[operationType]; + var newOperation = newOperationMap[operationType]; + + var diff = _openApiDiff + .OperationDiff + .Diff(oldOperation, newOperation, context.CopyWithMethod(operationType)); + + if (diff != null) + changedPath.Changed.Add(diff); + } + + changedPath.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context); + + return ChangedUtils.IsChanged(changedPath); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs new file mode 100644 index 000000000..0937f06bd --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class PathsDiff + { + private readonly OpenApiDiff _openApiDiff; + + public PathsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedPathsBO Diff(Dictionary left, Dictionary right) + { + var changedPaths = new ChangedPathsBO(left, right); + + foreach (var (key, value) in right) + { + changedPaths.Increased.Add(key, value); + } + + foreach (var (key, value) in left) + { + var template = key.NormalizePath(); + var result = right.Keys.FirstOrDefault(x => x.NormalizePath() == template); + + if (result != null) + { + if (!changedPaths.Increased.ContainsKey(result)) + throw new ArgumentException($"Two path items have the same signature: {template}"); + var rightPath = changedPaths.Increased[result]; + changedPaths.Increased.Remove(result); + var paramsDict = new Dictionary(); + if (key != result) + { + var oldParams = key.ExtractParametersFromPath(); + var newParams = result.ExtractParametersFromPath(); + for (var i = oldParams.Count - 1; i >= 0; i--) + { + paramsDict.Add(oldParams[i], newParams[i]); + } + } + var context = new DiffContextBO() + { + URL = key, + Parameters = paramsDict + }; + + var diff = _openApiDiff + .PathDiff + .Diff(value, rightPath, context); + + if (diff != null) + changedPaths.Changed.Add(result, diff); + } + else + { + changedPaths.Missing.Add(key, value); + } + } + + return ChangedUtils.IsChanged(changedPaths); + } + + public static OpenApiPaths ValOrEmpty(OpenApiPaths path) + { + return path ?? new OpenApiPaths(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs b/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs new file mode 100644 index 000000000..d7a9f5b9b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public abstract class ReferenceDiffCache + where TD : class + { + public Dictionary RefDiffMap { get; set; } + + protected ReferenceDiffCache() + { + RefDiffMap = new Dictionary(); + } + + protected string GetRefKey(string leftRef, string rightRef) + { + return leftRef + ":" + rightRef; + } + + protected abstract TD ComputeDiff( + HashSet refSet, TC left, TC right, DiffContextBO context); + + public TD CachedDiff( + HashSet refSet, + TC left, + TC right, + string leftRef, + string rightRef, + DiffContextBO context) + { + var areBothRefParameters = leftRef != null && rightRef != null; + if (areBothRefParameters) + { + var key = new CacheKey(leftRef, rightRef, context); + if (RefDiffMap.TryGetValue(key, out var changedFromRef)) + return changedFromRef; + + var refKey = GetRefKey(leftRef, rightRef); + if (refSet.Contains(refKey)) + return null; + + refSet.Add(refKey); + var changed = ComputeDiff(refSet, left, right, context); + RefDiffMap.Add(key, changed); + refSet.Remove(refKey); + return changed; + } + + return ComputeDiff(refSet, left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs new file mode 100644 index 000000000..e5e8a2712 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class RequestBodyDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.RequestBodies); + private readonly OpenApiDiff _openApiDiff; + + public RequestBodyDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + private static IDictionary GetExtensions(OpenApiRequestBody body) + { + return body.Extensions.ToDictionary(x => x.Key, x => x.Value); + } + + public ChangedRequestBodyBO Diff( + OpenApiRequestBody left, OpenApiRequestBody right, DiffContextBO context) + { + var leftRef = left.Reference?.ReferenceV3; + var rightRef = right.Reference?.ReferenceV3; + return CachedDiff(new HashSet(), left, right, leftRef, rightRef, context); + } + + protected override ChangedRequestBodyBO ComputeDiff(HashSet refSet, OpenApiRequestBody left, OpenApiRequestBody right, + DiffContextBO context) + { + Dictionary oldRequestContent = null; + Dictionary newRequestContent = null; + OpenApiRequestBody oldRequestBody = null; + OpenApiRequestBody newRequestBody = null; + if (left != null) + { + oldRequestBody = + RefPointer.ResolveRef( + _openApiDiff.OldSpecOpenApi.Components, left, left.Reference?.ReferenceV3); + if (oldRequestBody.Content != null) + { + oldRequestContent = (Dictionary) oldRequestBody.Content; + } + } + if (right != null) + { + newRequestBody = + RefPointer.ResolveRef( + _openApiDiff.NewSpecOpenApi.Components, right, right.Reference?.ReferenceV3); + if (newRequestBody.Content != null) + { + newRequestContent = (Dictionary) newRequestBody.Content; + } + } + var leftRequired = + oldRequestBody != null && oldRequestBody.Required; + var rightRequired = + newRequestBody != null && newRequestBody.Required; + + var changedRequestBody = + new ChangedRequestBodyBO(oldRequestBody, newRequestBody, context) + { + ChangeRequired = leftRequired != rightRequired, + Description = _openApiDiff + .MetadataDiff + .Diff( + oldRequestBody?.Description, + newRequestBody?.Description, + context), + Content = _openApiDiff + .ContentDiff + .Diff(oldRequestContent, newRequestContent, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(GetExtensions(left), GetExtensions(right), context) + }; + + return ChangedUtils.IsChanged(changedRequestBody); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs new file mode 100644 index 000000000..436551a3e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ResponseDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Responses); + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public ResponseDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedResponseBO Diff(OpenApiResponse left, OpenApiResponse right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedResponseBO ComputeDiff(HashSet refSet, OpenApiResponse left, OpenApiResponse right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedResponse = new ChangedResponseBO(left, right, context) + { + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Headers = _openApiDiff + .HeadersDiff + .Diff(left.Headers, right.Headers, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedResponse); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs new file mode 100644 index 000000000..d29745b0b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Compare.SchemaDiffResult; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SchemaDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Schemas); + + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + + public SchemaDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public static SchemaDiffResult.SchemaDiffResult GetSchemaDiffResult(OpenApiDiff openApiDiff) + { + return GetSchemaDiffResult(null, openApiDiff); + } + + public static SchemaDiffResult.SchemaDiffResult GetSchemaDiffResult(OpenApiSchema schema, OpenApiDiff openApiDiff) + { + switch (schema.GetSchemaType()) + { + case SchemaTypeEnum.Schema: + return new SchemaDiffResult.SchemaDiffResult(openApiDiff); + case SchemaTypeEnum.ArraySchema: + return new ArraySchemaDiffResult(openApiDiff); + case SchemaTypeEnum.ComposedSchema: + return new ComposedSchemaDiffResult(openApiDiff); + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected static OpenApiSchema ResolveComposedSchema(OpenApiComponents components, OpenApiSchema schema) + { + if (schema != null && schema.GetSchemaType() == SchemaTypeEnum.ComposedSchema) + { + var allOfSchemaList = schema.AllOf; + if (!allOfSchemaList.IsNullOrEmpty()) + { + var refName = "allOfCombined-"; + allOfSchemaList + .ToList() + .ForEach(x => refName += x.Reference?.ReferenceV3); + if (components.Schemas.ContainsKey(refName)) + return components.Schemas[refName]; + components.Schemas.Add(refName, new OpenApiSchema()); + + var allOfCombinedSchema = new OpenApiSchema(); + allOfCombinedSchema = AddSchema(allOfCombinedSchema, schema); + foreach (var t in allOfSchemaList) + { + var allOfSchema = t; + allOfSchema = + RefPointer.ResolveRef(components, allOfSchema, allOfSchema.Reference?.ReferenceV3); + allOfSchema = ResolveComposedSchema(components, allOfSchema); + allOfCombinedSchema = AddSchema(allOfCombinedSchema, allOfSchema); + } + return allOfCombinedSchema; + } + } + return schema; + } + + protected static OpenApiSchema AddSchema(OpenApiSchema schema, OpenApiSchema fromSchema) + { + if (fromSchema.Properties != null) + { + if (schema.Properties == null) + { + schema.Properties = new Dictionary(); + } + + foreach (var property in fromSchema.Properties) + { + schema.Properties.Add(property); + } + } + + if (fromSchema.Required != null) + { + if (schema.Required == null) + { + schema.Required = fromSchema.Required; + } + else + { + foreach (var required in fromSchema.Required) + { + schema.Required.Add(required); + } + } + } + + schema.ReadOnly = fromSchema.ReadOnly; + schema.WriteOnly = fromSchema.WriteOnly; + schema.Deprecated = fromSchema.Deprecated; + schema.Nullable = fromSchema.Nullable; + + if (fromSchema.ExclusiveMaximum != null) + { + schema.ExclusiveMaximum = fromSchema.ExclusiveMaximum; + } + if (fromSchema.ExclusiveMinimum != null) + { + schema.ExclusiveMinimum = fromSchema.ExclusiveMinimum; + } + if (fromSchema.UniqueItems != null) + { + schema.UniqueItems = fromSchema.UniqueItems; + } + if (fromSchema.Description != null) + { + schema.Description = fromSchema.Description; + } + if (fromSchema.Format != null) + { + schema.Format = fromSchema.Format; + } + if (fromSchema.Type != null) + { + schema.Type = fromSchema.Type; + } + if (fromSchema.Enum != null) + { + if (schema.Enum == null) + { + schema.Enum = new List(); + } + //noinspection unchecked + foreach (var element in fromSchema.Enum) + { + schema.Enum.Add(element); + } + } + if (fromSchema.Extensions != null) + { + if (schema.Extensions == null) + { + schema.Extensions = new Dictionary(); + } + foreach (var element in fromSchema.Extensions) + { + schema.Extensions.Add(element); + } + } + if (fromSchema.Discriminator != null) + { + if (schema.Discriminator == null) + { + schema.Discriminator = new OpenApiDiscriminator(); + } + var discriminator = schema.Discriminator; + var fromDiscriminator = fromSchema.Discriminator; + + if (fromDiscriminator.PropertyName != null) + { + discriminator.PropertyName = fromDiscriminator.PropertyName; + } + if (fromDiscriminator.Mapping != null) + { + if (discriminator.Mapping == null) + { + discriminator.Mapping = new Dictionary(); + } + foreach (var element in fromDiscriminator.Mapping) + { + discriminator.Mapping.Add(element); + } + } + } + if (fromSchema.Title != null) + { + schema.Title = fromSchema.Title; + } + if (fromSchema.AdditionalProperties != null) + { + schema.AdditionalProperties = fromSchema.AdditionalProperties; + } + if (fromSchema.Default != null) + { + schema.Default = fromSchema.Default; + } + if (fromSchema.Example != null) + { + schema.Example = fromSchema.Example; + } + if (fromSchema.ExternalDocs != null) + { + if (schema.ExternalDocs == null) + { + schema.ExternalDocs = new OpenApiExternalDocs(); + } + var externalDocs = schema.ExternalDocs; + var fromExternalDocs = fromSchema.ExternalDocs; + if (fromExternalDocs.Description != null) + { + externalDocs.Description = fromExternalDocs.Description; + } + if (fromExternalDocs.Extensions != null) + { + if (externalDocs.Extensions == null) + { + externalDocs.Extensions = new Dictionary(); + } + + foreach (var element in fromSchema.Extensions) + { + schema.Extensions.Add(element); + } + } + if (fromExternalDocs.Url != null) + { + externalDocs.Url = fromExternalDocs.Url; + } + } + if (fromSchema.Maximum != null) + { + schema.Maximum = fromSchema.Maximum; + } + if (fromSchema.Minimum != null) + { + schema.Minimum = fromSchema.Minimum; + } + if (fromSchema.MaxItems != null) + { + schema.MaxItems = fromSchema.MaxItems; + } + if (fromSchema.MinItems != null) + { + schema.MinItems = fromSchema.MinItems; + } + if (fromSchema.MaxProperties != null) + { + schema.MaxProperties = fromSchema.MaxProperties; + } + if (fromSchema.MinProperties != null) + { + schema.MinProperties = fromSchema.MinProperties; + } + if (fromSchema.MaxLength != null) + { + schema.MaxLength = fromSchema.MaxLength; + } + if (fromSchema.MinLength != null) + { + schema.MinLength = fromSchema.MinLength; + } + if (fromSchema.MultipleOf != null) + { + schema.MultipleOf = fromSchema.MultipleOf; + } + if (fromSchema.Not != null) + { + if (schema.Not == null) + { + schema.Not = AddSchema(new OpenApiSchema(), fromSchema.Not); + } + else + { + AddSchema(schema.Not, fromSchema.Not); + } + } + if (fromSchema.Pattern != null) + { + schema.Pattern = fromSchema.Pattern; + } + if (fromSchema.Xml != null) + { + if (schema.Xml == null) + { + schema.Xml = new OpenApiXml(); + } + var xml = schema.Xml; + var fromXml = fromSchema.Xml; + + xml.Attribute = fromXml.Attribute; + + if (fromXml.Name != null) + { + xml.Name = fromXml.Name; + } + if (fromXml.Namespace != null) + { + xml.Namespace = fromXml.Namespace; + } + if (fromXml.Extensions != null) + { + if (xml.Extensions == null) + { + xml.Extensions = new Dictionary(); + } + foreach (var element in fromXml.Extensions) + { + xml.Extensions.Add(element); + } + } + if (fromXml.Prefix != null) + { + xml.Prefix = fromXml.Prefix; + } + + xml.Wrapped = fromXml.Wrapped; + } + return schema; + } + + private static string GetSchemaRef(OpenApiSchema schema) + { + return schema?.Reference?.ReferenceV3; + } + + public ChangedSchemaBO Diff(HashSet refSet, OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + if (left == null && right == null) + { + return null; + } + return CachedDiff(refSet, left, right, GetSchemaRef(left), GetSchemaRef(right), context); + } + + public ChangedSchemaBO GetTypeChangedSchema( + OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + var schemaDiffResult = GetSchemaDiffResult(_openApiDiff); + schemaDiffResult.ChangedSchema.OldSchema = left; + schemaDiffResult.ChangedSchema.NewSchema = right; + schemaDiffResult.ChangedSchema.IsChangedType = true; + schemaDiffResult.ChangedSchema.Context = context; + + return schemaDiffResult.ChangedSchema; + } + + protected override ChangedSchemaBO ComputeDiff( + HashSet refSet, OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, GetSchemaRef(left)); + right = RefPointer.ResolveRef(_rightComponents, right, GetSchemaRef(right)); + + left = ResolveComposedSchema(_leftComponents, left); + right = ResolveComposedSchema(_rightComponents, right); + + // If type of schemas are different, just set old & new schema, set changedType to true in + // SchemaDiffResult and + // return the object + if ((left == null || right == null) + || left.Type != right.Type + || left.Format != right.Format) + { + return GetTypeChangedSchema(left, right, context); + } + + // If schema type is same then get specific SchemaDiffResult and compare the properties + var result = GetSchemaDiffResult(right, _openApiDiff); + return result.Diff(refSet, _leftComponents, _rightComponents, left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs new file mode 100644 index 000000000..3351bc071 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class ArraySchemaDiffResult : SchemaDiffResult + { + public ArraySchemaDiffResult(OpenApiDiff openApiDiff) : base("array", openApiDiff) + { + } + + public override ChangedSchemaBO Diff(HashSet refSet, OpenApiComponents leftComponents, OpenApiComponents rightComponents, T left, + T right, DiffContextBO context) + { + if (left.GetSchemaType() != SchemaTypeEnum.ArraySchema + || right.GetSchemaType() != SchemaTypeEnum.ArraySchema) + return null; + + base.Diff(refSet, leftComponents, rightComponents, left, right, context); + + var diff = OpenApiDiff + .SchemaDiff + .Diff( + refSet, + left.Items, + right.Items, + context.CopyWithRequired(true)); + if (diff != null) + ChangedSchema.Items = diff; + + return IsApplicable(context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs new file mode 100644 index 000000000..8343604a6 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class ComposedSchemaDiffResult : SchemaDiffResult + { + private static RefPointer refPointer = new RefPointer(RefTypeEnum.Schemas); + + public ComposedSchemaDiffResult(OpenApiDiff openApiDiff) : base(openApiDiff) + { + } + + private static Dictionary GetSchema(OpenApiComponents components, Dictionary mapping) + { + var result = new Dictionary(); + foreach (var map in mapping) + { + result.Add(map.Key, refPointer.ResolveRef(components, new OpenApiSchema(), map.Value)); + } + return result; + } + + private static Dictionary GetMapping(OpenApiSchema composedSchema) + { + if (composedSchema.GetSchemaType() != SchemaTypeEnum.ComposedSchema) + return null; + + var reverseMapping = new Dictionary(); + foreach (var schema in composedSchema.OneOf) + { + var schemaRef = schema.Reference?.ReferenceV3; + if (schemaRef == null) + { + throw new ArgumentNullException("invalid oneOf schema"); + } + var schemaName = refPointer.GetRefName(schemaRef); + if (schemaName == null) + { + throw new ArgumentNullException("invalid schema: " + schemaRef); + } + reverseMapping.Add(schemaRef, schemaName); + } + + if (!composedSchema.Discriminator.Mapping.IsNullOrEmpty()) + { + foreach (var (key, value) in composedSchema.Discriminator.Mapping) + { + if (!reverseMapping.TryAdd(value, key)) + reverseMapping[value] = key; + } + } + + return reverseMapping.ToDictionary(x => x.Value, x => x.Key); + } + + public override ChangedSchemaBO Diff(HashSet refSet, OpenApiComponents leftComponents, OpenApiComponents rightComponents, T left, + T right, DiffContextBO context) + { + if (left.GetSchemaType() == SchemaTypeEnum.ComposedSchema) + { + if (!left.OneOf.IsNullOrEmpty() || !right.OneOf.IsNullOrEmpty()) + { + var leftDis = left.Discriminator; + var rightDis = right.Discriminator; + if (leftDis == null + || rightDis == null + || leftDis.PropertyName == null + || rightDis.PropertyName == null) + { + throw new ArgumentException( + "discriminator or property not found for oneOf schema"); + } + + if (leftDis.PropertyName != rightDis.PropertyName + || left.OneOf.IsNullOrEmpty() + || right.OneOf.IsNullOrEmpty()) + { + ChangedSchema.OldSchema = left; + ChangedSchema.NewSchema = right; + ChangedSchema.DiscriminatorPropertyChanged = true; + ChangedSchema.Context = context; + return ChangedSchema; + } + + var leftMapping = GetMapping(left); + var rightMapping = GetMapping(right); + + var mappingDiff = MapKeyDiff.Diff(GetSchema(leftComponents, leftMapping), GetSchema(rightComponents, rightMapping)); + var changedMapping = new Dictionary(); + foreach (var refId in mappingDiff.SharedKey) + { + var leftReference = leftComponents.Schemas.Values + .First(x => x.Reference.ReferenceV3 == leftMapping[refId]).Reference; + var rightReference = rightComponents.Schemas.Values + .First(x => x.Reference.ReferenceV3 == rightMapping[refId]).Reference; + + var leftSchema = new OpenApiSchema { Reference = leftReference }; + var rightSchema = new OpenApiSchema { Reference = rightReference }; + var changedSchema = OpenApiDiff.SchemaDiff + .Diff(refSet, leftSchema, rightSchema, context.CopyWithRequired(true)); + if (changedSchema != null) + changedMapping.Add(refId, changedSchema); + } + + ChangedSchema.OneOfSchema = new ChangedOneOfSchemaBO(leftMapping, rightMapping, context) + { + Increased = mappingDiff.Increased, + Missing = mappingDiff.Missing, + Changed = changedMapping + }; + } + return base.Diff(refSet, leftComponents, rightComponents, left, right, context); + } + + return OpenApiDiff.SchemaDiff.GetTypeChangedSchema(left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs new file mode 100644 index 000000000..c1290fb39 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class SchemaDiffResult + { + public ChangedSchemaBO ChangedSchema { get; set; } + public OpenApiDiff OpenApiDiff { get; set; } + + public SchemaDiffResult(OpenApiDiff openApiDiff) + { + OpenApiDiff = openApiDiff; + ChangedSchema = new ChangedSchemaBO(); + } + + public SchemaDiffResult(string type, OpenApiDiff openApiDiff) : this(openApiDiff) + { + ChangedSchema.Type = type; + } + + public virtual ChangedSchemaBO Diff( + HashSet refSet, + OpenApiComponents leftComponents, + OpenApiComponents rightComponents, + T left, + T right, + DiffContextBO context) + where T : OpenApiSchema + { + var leftEnumStrings = left.Enum.Select(x => ((IOpenApiPrimitive)x)?.GetValueString()).ToList(); + var rightEnumStrings = right.Enum.Select(x => ((IOpenApiPrimitive)x)?.GetValueString()).ToList(); + var leftDefault= (IOpenApiPrimitive)left.Default; + var rightDefault = (IOpenApiPrimitive)right.Default; + + var changedEnum = + ListDiff.Diff(new ChangedEnumBO(leftEnumStrings, rightEnumStrings, context)); + + ChangedSchema.Context = context; + ChangedSchema.OldSchema = left; + ChangedSchema.NewSchema = right; + ChangedSchema.IsChangeDeprecated = !left.Deprecated && right.Deprecated; + ChangedSchema.IsChangeTitle = left.Title != right.Title; + ChangedSchema.Required = ListDiff.Diff(new ChangedRequiredBO(left.Required.ToList(), right.Required.ToList(), context)); + ChangedSchema.IsChangeDefault = leftDefault?.GetValueString() != rightDefault?.GetValueString(); + ChangedSchema.Enumeration = changedEnum; + ChangedSchema.IsChangeFormat = left.Format != right.Format; + ChangedSchema.ReadOnly = new ChangedReadOnlyBO(left.ReadOnly, right.ReadOnly, context); + ChangedSchema.WriteOnly = new ChangedWriteOnlyBO(left.WriteOnly, right.WriteOnly, context); + ChangedSchema.MaxLength = new ChangedMaxLengthBO(left.MaxLength, right.MaxLength, context); + + var extendedDiff = OpenApiDiff.ExtensionsDiff.Diff(left.Extensions, right.Extensions, context); + if (extendedDiff != null) + ChangedSchema.Extensions = extendedDiff; + var metaDataDiff = OpenApiDiff.MetadataDiff.Diff(left.Description, right.Description, context); + if (metaDataDiff != null) + ChangedSchema.Description = metaDataDiff; + + var leftProperties = left.Properties; + var rightProperties = right.Properties; + var propertyDiff = MapKeyDiff.Diff(leftProperties, rightProperties); + + foreach (var s in propertyDiff.SharedKey) + { + var diff = OpenApiDiff + .SchemaDiff + .Diff(refSet, leftProperties[s], rightProperties[s], Required(context, s, right.Required)); + + if (diff != null) + ChangedSchema.ChangedProperties.Add(s, diff); + } + + CompareAdditionalProperties(refSet, left, right, context); + + var allIncreasedProperties = FilterProperties(TypeEnum.Added, propertyDiff.Increased, context); + foreach (var (key, value) in allIncreasedProperties) + { + ChangedSchema.IncreasedProperties.Add(key, value); + } + var allMissingProperties = FilterProperties(TypeEnum.Removed, propertyDiff.Missing, context); + foreach (var (key, value) in allMissingProperties) + { + ChangedSchema.MissingProperties.Add(key, value); + } + + return IsApplicable(context); + } + + private static DiffContextBO Required(DiffContextBO context, string key, ICollection required) + { + return context.CopyWithRequired(required != null && required.Contains(key)); + } + + private void CompareAdditionalProperties(HashSet refSet, OpenApiSchema leftSchema, OpenApiSchema rightSchema, DiffContextBO context) + { + var left = leftSchema.AdditionalProperties; + var right = rightSchema.AdditionalProperties; + if (left != null || right != null) + { + var apChangedSchema = new ChangedSchemaBO + { + Context = context, + OldSchema = left, + NewSchema = right + }; + if (left != null && right != null) + { + var addPropChangedSchemaOp = + OpenApiDiff + .SchemaDiff + .Diff(refSet, left, right, context.CopyWithRequired(false)); + apChangedSchema = addPropChangedSchemaOp ?? apChangedSchema; + } + var changed = ChangedUtils.IsChanged(apChangedSchema); + if (changed != null) + ChangedSchema.AddProp = changed; + } + } + private Dictionary FilterProperties(TypeEnum type, Dictionary properties, DiffContextBO context) + { + var result = new Dictionary(); + + foreach (var (key, value) in properties) + { + if (IsPropertyApplicable(value, context) + && OpenApiDiff + .ExtensionsDiff.IsParentApplicable(type, + value, + value?.Extensions ?? new Dictionary(), + context)) + { + result.Add(key, value); + } + else + { + // Child property is not applicable, so required cannot be applied + ChangedSchema.Required.Increased.Remove(key); + } + } + + + return result; + } + + private static bool IsPropertyApplicable(OpenApiSchema schema, DiffContextBO context) + { + return !(context.IsResponse && schema.WriteOnly) && !(context.IsRequest && schema.ReadOnly); + } + + protected ChangedSchemaBO IsApplicable(DiffContextBO context) + { + if (ChangedSchema.ReadOnly.IsUnchanged() + && ChangedSchema.WriteOnly.IsUnchanged() + && !IsPropertyApplicable(ChangedSchema.NewSchema, context)) + { + return null; + } + return ChangedUtils.IsChanged(ChangedSchema); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs new file mode 100644 index 000000000..17a5c018b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecurityRequirementDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public SecurityRequirementDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + public static OpenApiSecurityRequirement GetCopy(Dictionary> right) + { + var newSecurityRequirement = new OpenApiSecurityRequirement(); + foreach (var (key, value) in right) + { + newSecurityRequirement.Add(key, value); + } + + return newSecurityRequirement; + } + + private OpenApiSecurityRequirement Contains( + OpenApiSecurityRequirement right, string schemeRef) + { + var leftSecurityScheme = _leftComponents.SecuritySchemes[schemeRef]; + var found = new OpenApiSecurityRequirement(); + + foreach (var keyValuePair in right) + { + var rightSecurityScheme = _rightComponents.SecuritySchemes[keyValuePair.Key.Reference?.ReferenceV3]; + if (leftSecurityScheme.Type == rightSecurityScheme.Type) + { + switch (leftSecurityScheme.Type) + { + case SecuritySchemeType.ApiKey: + if (leftSecurityScheme.Name == rightSecurityScheme.Name) + { + found.Add(keyValuePair.Key, keyValuePair.Value); + return found; + } + + break; + case SecuritySchemeType.Http: + case SecuritySchemeType.OAuth2: + case SecuritySchemeType.OpenIdConnect: + found.Add(keyValuePair.Key, keyValuePair.Value); + return found; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + return found; + } + + public ChangedSecurityRequirementBO Diff( + OpenApiSecurityRequirement left, OpenApiSecurityRequirement right, DiffContextBO context) + { + var changedSecurityRequirement = + new ChangedSecurityRequirementBO(left, right != null ? GetCopy(right) : null); + + left ??= new OpenApiSecurityRequirement(); + right ??= new OpenApiSecurityRequirement(); + + foreach (var (key, value) in left) + { + var rightSec = Contains(right, key.Reference?.ReferenceV3); + if (rightSec.IsNullOrEmpty()) + { + changedSecurityRequirement.Missing.Add(key, value); + } + else + { + var rightSchemeRef = rightSec.Keys.First(); + right.Remove(rightSchemeRef); + var diff = + _openApiDiff + .SecuritySchemeDiff + .Diff( + key.Reference?.ReferenceV3, + value.ToList(), + rightSchemeRef.Reference?.ReferenceV3, + rightSec[rightSchemeRef].ToList(), + context); + if (diff != null) + changedSecurityRequirement.Changed.Add(diff); + } + } + + foreach (var (key, value) in right) + { + changedSecurityRequirement.Increased.Add(key, value); + } + + return ChangedUtils.IsChanged(changedSecurityRequirement); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs new file mode 100644 index 000000000..c54c8197d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecurityRequirementsDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private static RefPointer _refPointer = new RefPointer(RefTypeEnum.SecuritySchemes); + + public SecurityRequirementsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + public OpenApiSecurityRequirement Contains(IList securityRequirements, OpenApiSecurityRequirement left) + { + return securityRequirements + .FirstOrDefault(x => Same(left, x)); + } + + public bool Same(OpenApiSecurityRequirement left, OpenApiSecurityRequirement right) + { + var leftTypes = GetListOfSecuritySchemes(_leftComponents, left); + var rightTypes = GetListOfSecuritySchemes(_rightComponents, right); + + return leftTypes.SequenceEqual(rightTypes); + } + + private static ImmutableDictionary GetListOfSecuritySchemes( + OpenApiComponents components, OpenApiSecurityRequirement securityRequirement) + { + var tmpResult = new Dictionary(); + foreach (var openApiSecurityScheme in securityRequirement.Keys.ToList()) + { + + if (components.SecuritySchemes.TryGetValue(openApiSecurityScheme.Reference?.ReferenceV3, out var result)) + { + if (!tmpResult.ContainsKey(result.Type)) + tmpResult.Add(result.Type, result.In); + } + else + { + throw new ArgumentException("Impossible to find security scheme: " + openApiSecurityScheme.Scheme); + } + } + return tmpResult.ToImmutableDictionary(); + } + + public ChangedSecurityRequirementsBO Diff( + IList left, IList right, DiffContextBO context) + { + left ??= new List(); + right = right != null ? GetCopy(right) : new List(); + + var changedSecurityRequirements = new ChangedSecurityRequirementsBO(left, right); + + foreach (var leftSecurity in left) + { + var rightSecOpt = Contains(right, leftSecurity); + if (rightSecOpt == null) + { + changedSecurityRequirements.Missing.Add(leftSecurity); + } + else + { + var rightSec = rightSecOpt; + + right.Remove(rightSec); + var diff = + _openApiDiff. + SecurityRequirementDiff + .Diff(leftSecurity, rightSec, context); + if (diff != null) + changedSecurityRequirements.Changed.Add(diff); + } + } + + changedSecurityRequirements.Increased.AddRange(right); + + return ChangedUtils.IsChanged(changedSecurityRequirements); + } + + private static List GetCopy(IEnumerable right) + { + return right.Select(SecurityRequirementDiff.GetCopy).ToList(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs new file mode 100644 index 000000000..5906767bc --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecuritySchemeDiff : ReferenceDiffCache + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public SecuritySchemeDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedSecuritySchemeBO Diff( + string leftSchemeRef, + List leftScopes, + string rightSchemeRef, + List rightScopes, + DiffContextBO context) + { + var leftSecurityScheme = _leftComponents.SecuritySchemes[leftSchemeRef]; + var rightSecurityScheme = _rightComponents.SecuritySchemes[rightSchemeRef]; + var changedSecuritySchemeOpt = + CachedDiff( + new HashSet(), + leftSecurityScheme, + rightSecurityScheme, + leftSchemeRef, + rightSchemeRef, + context); + var changedSecurityScheme = + changedSecuritySchemeOpt ?? new ChangedSecuritySchemeBO(leftSecurityScheme, rightSecurityScheme); + changedSecurityScheme = GetCopyWithoutScopes(changedSecurityScheme); + + if (changedSecurityScheme != null + && leftSecurityScheme.Type == SecuritySchemeType.OAuth2) + { + var changed = ChangedUtils.IsChanged(ListDiff.Diff( + new ChangedSecuritySchemeScopesBO(leftScopes, rightScopes) + )); + + if (changed != null) + changedSecurityScheme.ChangedScopes = changed; + } + + return ChangedUtils.IsChanged(changedSecurityScheme); + } + + protected override ChangedSecuritySchemeBO ComputeDiff( + HashSet refSet, + OpenApiSecurityScheme leftSecurityScheme, + OpenApiSecurityScheme rightSecurityScheme, + DiffContextBO context) + { + var changedSecurityScheme = + new ChangedSecuritySchemeBO(leftSecurityScheme, rightSecurityScheme) + { + Description = _openApiDiff + .MetadataDiff + .Diff(leftSecurityScheme.Description, rightSecurityScheme.Description, context) + }; + + switch (leftSecurityScheme.Type) + { + case SecuritySchemeType.ApiKey: + changedSecurityScheme.IsChangedIn = + !leftSecurityScheme.In.Equals(rightSecurityScheme.In); + break; + case SecuritySchemeType.Http: + changedSecurityScheme.IsChangedScheme = + leftSecurityScheme.Scheme != rightSecurityScheme.Scheme; + changedSecurityScheme.IsChangedBearerFormat = + leftSecurityScheme.BearerFormat != rightSecurityScheme.BearerFormat; + break; + case SecuritySchemeType.OAuth2: + changedSecurityScheme.OAuthFlows = _openApiDiff + .OAuthFlowsDiff + .Diff(leftSecurityScheme.Flows, rightSecurityScheme.Flows); + break; + case SecuritySchemeType.OpenIdConnect: + changedSecurityScheme.IsChangedOpenIdConnectUrl = + leftSecurityScheme.OpenIdConnectUrl != rightSecurityScheme.OpenIdConnectUrl; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + changedSecurityScheme.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(leftSecurityScheme.Extensions, rightSecurityScheme.Extensions, context); + + return changedSecurityScheme; + } + + private static ChangedSecuritySchemeBO GetCopyWithoutScopes(ChangedSecuritySchemeBO original) + { + return new ChangedSecuritySchemeBO( + original.OldSecurityScheme, original.NewSecurityScheme) + { + IsChangedType = original.IsChangedType, + IsChangedIn = original.IsChangedIn, + IsChangedScheme = original.IsChangedScheme, + IsChangedBearerFormat = original.IsChangedBearerFormat, + Description = original.Description, + OAuthFlows = original.OAuthFlows, + IsChangedOpenIdConnectUrl = original.IsChangedOpenIdConnectUrl + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs new file mode 100644 index 000000000..f52828d65 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs @@ -0,0 +1,31 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum ChangedElementTypeEnum + { + OpenApi, + Operation, + RequestBody, + Path, + Content, + Response, + Request, + Parameter, + Schema, + OneOf, + AnyOf, + AllOf, + Header, + SecurityRequirement, + SecurityScheme, + SecuritySchemeScope, + AuthFlow, + Metadata, + MediaType, + WriteOnly, + ReadOnly, + MaxLength, + Required, + Extension, + Enum + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs new file mode 100644 index 000000000..2e685e14f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs @@ -0,0 +1,11 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum DiffResultEnum + { + NoChanges = 0, + Metadata = 1, + Compatible = 2, + Unknown = 3, + Incompatible = 4 + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs new file mode 100644 index 000000000..ee9ea286c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs @@ -0,0 +1,12 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum RefTypeEnum + { + RequestBodies, + Responses, + Parameters, + Schemas, + Headers, + SecuritySchemes + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs new file mode 100644 index 000000000..289011f12 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs @@ -0,0 +1,9 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum SchemaTypeEnum + { + Schema, + ArraySchema, + ComposedSchema + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs new file mode 100644 index 000000000..8696bbdc7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs @@ -0,0 +1,9 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum TypeEnum + { + Added, + Changed, + Removed + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs new file mode 100644 index 000000000..aa3f7fd44 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs @@ -0,0 +1,17 @@ +using System.IO; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class IOpenApiPrimitiveExtensions + { + public static string GetValueString(this IOpenApiPrimitive primitive) + { + using var sb = new StringWriter(); + var writer = new OpenApiYamlWriter(sb); + primitive.Write(writer, OpenApiSpecVersion.OpenApi3_0); + return sb.ToString(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs new file mode 100644 index 000000000..f8b143505 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class ListExtensions + { + /// + /// Determines whether the collection is null or contains no elements. + /// + /// The IEnumerable type. + /// The enumerable, which may be null or empty. + /// + /// true if the IEnumerable is null or empty; otherwise, false. + /// + public static bool IsNullOrEmpty(this IEnumerable enumerable) + { + if (enumerable == null) + { + return true; + } + /* If this is a list, use the Count property for efficiency. + * The Count property is O(1) while IEnumerable.Count() is O(N). */ + var collection = enumerable as ICollection; + if (collection != null) + { + return collection.Count < 1; + } + return !enumerable.Any(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs new file mode 100644 index 000000000..199a99b35 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class OpenApiSchemaExtensions + { + public static SchemaTypeEnum GetSchemaType(this OpenApiSchema schema) + { + if (schema == null) + return SchemaTypeEnum.Schema; + + if (schema.Items != null) + return SchemaTypeEnum.ArraySchema; + + if (!schema.AnyOf.IsNullOrEmpty() || !schema.OneOf.IsNullOrEmpty() || !schema.AllOf.IsNullOrEmpty()) + return SchemaTypeEnum.ComposedSchema; + + return SchemaTypeEnum.Schema; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs new file mode 100644 index 000000000..542353181 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class PathExtensions + { + private const string RegexPath = "\\{([^/]+)\\}"; + + public static string NormalizePath(this string path) + { + return Regex.Replace(path, RegexPath, "{}"); + } + + public static List ExtractParametersFromPath(this string path) + { + var paramsList = new List(); + var reg = new Regex(RegexPath); + var matches = reg.Matches(path); + if (!matches.IsNullOrEmpty()) + { + foreach (Match m in matches) + paramsList.Add(m.Groups[1].Value); + } + return paramsList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs b/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs new file mode 100644 index 000000000..434faea09 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs @@ -0,0 +1,12 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace Microsoft.OpenApi.Diff +{ + public interface IOpenAPICompare + { + ChangedOpenApiBO FromLocations(string oldLocation, string newLocation, OpenApiReaderSettings settings = null); + ChangedOpenApiBO FromSpecifications(OpenApiDocument oldSpec, string oldSpecIdentifier, OpenApiDocument newSpec, string newSpecIdentifier); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj b/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj new file mode 100644 index 000000000..bc6963338 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj @@ -0,0 +1,44 @@ + + + + netstandard2.1 + true + http://go.microsoft.com/fwlink/?LinkID=288890 + https://github.com/Microsoft/OpenAPI.NET + https://raw.githubusercontent.com/Microsoft/OpenAPI.NET/master/LICENSE + true + Microsoft + Microsoft + Microsoft.OpenApi.Diff + Microsoft.OpenApi.Diff + 1.0.0 + Compare diffs between two OpenAPI specifications + © Microsoft Corporation. All rights reserved. + OpenAPI .NET Diff + + Microsoft.OpenApi.Diff + Microsoft.OpenApi.Diff + true + + latest + true + true + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs b/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs new file mode 100644 index 000000000..e04798a9d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Compare; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace Microsoft.OpenApi.Diff +{ + public class OpenAPICompare : IOpenAPICompare + { + private readonly ILogger _logger; + private readonly IEnumerable _extensions; + + public OpenAPICompare(ILogger logger, IEnumerable extensions) + { + _logger = logger; + _extensions = extensions; + } + + public ChangedOpenApiBO FromLocations(string oldLocation, string newLocation, OpenApiReaderSettings settings = null) + { + return FromLocations(oldLocation, Path.GetFileNameWithoutExtension(oldLocation), newLocation, Path.GetFileNameWithoutExtension(newLocation), settings); + } + + public ChangedOpenApiBO FromLocations(string oldLocation, string oldIdentifier, string newLocation, string newIdentifier, OpenApiReaderSettings settings = null) + { + return FromSpecifications(ReadLocation(oldLocation, settings: settings), oldIdentifier, ReadLocation(newLocation, settings: settings), newIdentifier); + } + + public ChangedOpenApiBO FromSpecifications(OpenApiDocument oldSpec, string oldSpecIdentifier, OpenApiDocument newSpec, string newSpecIdentifier) + { + return OpenApiDiff.Compare(oldSpec, oldSpecIdentifier, newSpec, newSpecIdentifier, _extensions, _logger); + } + + private static OpenApiDocument ReadLocation(string location, List auths = null, OpenApiReaderSettings settings = null) + { + using var sr = new StreamReader(location); + + var openAPIDoc = new OpenApiStreamReader(settings).Read(sr.BaseStream, out var diagnostic); + if (!diagnostic.Errors.IsNullOrEmpty()) + throw new Exception($"Error reading file. Error: {string.Join(", ", diagnostic.Errors)}"); + + return openAPIDoc; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs b/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs new file mode 100644 index 000000000..749260a0e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Output +{ + public abstract class BaseRenderer + { + protected RenderViewModel GetRenderModel(ChangedOpenApiBO diff, + string reportName = "OpenAPI Compatibility Report", + string logoUrl = "", + string pageTitle = "Api Change Log", + string pageDescription = "This report was generated by Microsoft.OpenApi.Diff") + { + return new RenderViewModel + { + PageTitle = pageTitle, + Author = "Microsoft.OpenApi.Diff", + Description = pageDescription, + Name = reportName, + LogoUrl = logoUrl, + ChangeType = diff.IsChanged(), + OldSpecIdentifier = diff.OldSpecIdentifier, + NewSpecIdentifier = diff.NewSpecIdentifier, + NewEndpoints = diff.NewEndpoints + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + MissingEndpoints = diff.MissingEndpoints + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + DeprecatedEndpoints = diff.GetDeprecatedEndpoints() + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + ChangedEndpoints = diff.ChangedOperations + .Select(x => new ChangedEndpointViewModel + { + Method = x.HttpMethod, + PathUrl = x.PathUrl, + Summary = x.Description?.Right ?? x.Summary?.Right, + ChangeType = x.IsChanged(), + ChangesByType = x.GetAllChangeInfoFlat(null) + .Where(y => !y.ChangeType.IsUnchanged()) + .Select(y => new ChangeViewModel + { + Path = y.Path.Where(z => !z.IsNullOrEmpty()).ToList(), + ChangeType = y.ChangeType, + Changes = y.Changes + .Select(z => new SingleChangeViewModel + { + ElementType = z.ElementType, + ChangeType = z.ChangeType, + FieldName = z.FieldName, + NewValue = z.NewValue, + OldValue = z.OldValue + }) + .ToList() + }) + .ToList() + }) + .OrderByDescending(x => x.ChangeType.DiffResult) + .ThenBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs b/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs new file mode 100644 index 000000000..12dd8e26b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Output +{ + public class ConsoleRender : IConsoleRender + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Schemas); + private static ChangedOpenApiBO _diff; + + private StringBuilder _output; + + public Task Render(ChangedOpenApiBO diff) + { + _diff = diff; + _output = new StringBuilder(); + if (diff.IsUnchanged()) + { + _output.Append("No differences. Specifications are equivalents"); + } + else + { + _output + .Append(Environment.NewLine) + .Append(BigTitle("Api Change Log")) + .Append(Center(diff.NewSpecOpenApi.Info.Title)) + .Append(Environment.NewLine); + + var newEndpoints = diff.NewEndpoints; + var olNewEndpoint = ListEndpoints(newEndpoints, "What's New"); + + var missingEndpoints = diff.MissingEndpoints; + var olMissingEndpoint = ListEndpoints(missingEndpoints, "What's Deleted"); + + var deprecatedEndpoints = diff.GetDeprecatedEndpoints(); + var olDeprecatedEndpoint = ListEndpoints(deprecatedEndpoints, "What's Deprecated"); + + var changedOperations = diff.ChangedOperations; + var olChanged = OlChanged(changedOperations); + + _output + .Append(renderBody(olNewEndpoint, olMissingEndpoint, olDeprecatedEndpoint, olChanged)) + .Append(Title("Result")) + .Append( + Center( + diff.IsCompatible() + ? "API changes are backward compatible" + : "API changes broke backward compatibility")) + .Append(Environment.NewLine) + .Append(Separator('-')); + } + return Task.FromResult(_output.ToString()); + } + private static string ListEndpoints(IReadOnlyCollection endpoints, string title) + { + if (null == endpoints || endpoints.Count == 0) return ""; + var sb = new StringBuilder(); + sb.Append(Title(title)); + foreach (var endpoint in endpoints) + { + sb.Append(ItemEndpoint( + endpoint.Method.ToString(), endpoint.PathUrl, endpoint.Summary)); + } + return sb.Append(Environment.NewLine).ToString(); + } + private static string ItemEndpoint(string method, string path, string desc) + { + var sb = new StringBuilder(); + sb.Append($"- {method,6} {path}{Environment.NewLine}"); + return sb.ToString(); + } + private static string renderBody(string olNew, string olMiss, string olDeprecated, string olChanged) + { + var sb = new StringBuilder(); + sb.Append(olNew).Append(olMiss).Append(olDeprecated).Append(olChanged); + return sb.ToString(); + } + private static string BigTitle(string title) + { + const char ch = '='; + return Title(title.ToUpper(), ch); + } + private static string Title(string title, char ch = '-') + { + var little = new string(ch, 2); + var offset = little.Length * 2; + + return $"{Separator(ch)}{little}{Center(title, -offset)}{little.PadLeft(Console.WindowWidth / 2 - title.Length / 2 + little.Length)}{Environment.NewLine}{Separator(ch)}"; + } + private static StringBuilder Separator(char ch) + { + var sb = new StringBuilder(); + return sb.Append(new string(ch, Console.WindowWidth)) + .Append(Environment.NewLine); + } + private static string Center(string center, int offset = 0) + { + return string.Format("{0," + (Console.WindowWidth / 2 + center.Length / 2 + offset) + "}", center); + } + private static string OlChanged(IReadOnlyCollection operations) + { + if (null == operations || operations.Count == 0) return ""; + var sb = new StringBuilder(); + sb.Append(Title("What's Changed")); + foreach (var operation in operations) + { + var pathUrl = operation.PathUrl; + var method = operation.HttpMethod.ToString(); + var desc = operation.Summary?.Right ?? ""; + + var ul_detail = new StringBuilder(); + if (ChangedBO.Result(operation.Parameters).IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Parameter:") + .Append(Environment.NewLine) + .Append(UlParam(operation.Parameters)); + } + if (operation.ResultRequestBody().IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Request:") + .Append(Environment.NewLine) + .Append(UlContent(operation.RequestBody.Content, true)); + } + if (operation.ResultApiResponses().IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Return Type:") + .Append(Environment.NewLine) + .Append(UlResponse(operation.APIResponses)); + } + sb.Append(ItemEndpoint(method, pathUrl, desc)).Append(ul_detail); + } + + return sb.ToString(); + } + private static string UlParam(ChangedParametersBO changedParameters) + { + var addParameters = changedParameters.Increased; + var delParameters = changedParameters.Missing; + var changed = changedParameters.Changed; + var sb = new StringBuilder(); + foreach (var param in addParameters) + { + sb.Append(ItemParam("Add ", param)); + } + foreach (var param in changed) + { + sb.Append(LiChangedParam(param)); + } + foreach (var param in delParameters) + { + sb.Append(ItemParam("Delete ", param)); + + } + return sb.ToString(); + } + private static string UlResponse(ChangedAPIResponseBO changedApiResponse) + { + var addResponses = changedApiResponse.Increased; + var delResponses = changedApiResponse.Missing; + var changedResponses = changedApiResponse.Changed; + var sb = new StringBuilder(); + foreach (var propName in addResponses.Keys) + { + sb.Append(ItemResponse("Add ", propName)); + } + foreach (var propName in delResponses.Keys) + { + sb.Append(ItemResponse("Deleted ", propName)); + } + foreach (var propName in changedResponses.Keys) + { + sb.Append(ItemChangedResponse("Changed ", propName, changedResponses[propName])); + } + return sb.ToString(); + } + private static string ItemResponse(string title, string code) + { + var sb = new StringBuilder(); + var status = ""; + if (code != "default" && int.TryParse(code, out var statusCode)) + { + status = ((HttpStatusCode)statusCode).ToString(); + } + sb.Append(new string(' ', 4)) + .Append("- ") + .Append(title) + .Append(code) + .Append(' ') + .Append(status) + .Append(Environment.NewLine); + return sb.ToString(); + } + private static string ItemParam(string title, OpenApiParameter param) + { + var sb = new StringBuilder(""); + sb.Append(new string(' ', 4)) + .Append("- ") + .Append(title) + .Append(param.Name) + .Append(" in ") + .Append(param.In) + .Append(Environment.NewLine); + + return sb.ToString(); + } + private static string LiChangedParam(ChangedParameterBO changeParam) + { + return ItemParam(changeParam.IsDeprecated ? "Deprecated " : "Changed ", changeParam.NewParameter); + } + private static string ItemChangedResponse(string title, string contentType, ChangedResponseBO response) + { + var sb = new StringBuilder(); + sb.Append(ItemResponse(title, contentType)); + sb.Append(new string(' ', 6)).Append("Media types:").Append(Environment.NewLine); + sb.Append(UlContent(response.Content, false)); + return sb.ToString(); + } + private static string UlContent(ChangedContentBO changedContent, bool isRequest) + { + var sb = new StringBuilder(); + if (changedContent == null) + { + return sb.ToString(); + } + foreach (var propName in changedContent.Increased.Keys) + { + sb.Append(ItemContent("Added ", propName)); + } + foreach (var propName in changedContent.Missing.Keys) + { + sb.Append(ItemContent("Deleted ", propName)); + } + foreach (var propName in changedContent.Changed.Keys) + { + sb.Append(ItemContent("Changed ", propName, changedContent.Changed[propName], isRequest)); + } + return sb.ToString(); + } + private static string ItemContent(string title, string contentType) + { + var sb = new StringBuilder(); + sb.Append(new string(' ', 8)) + .Append("- ") + .Append(title) + .Append(contentType) + .Append(Environment.NewLine); + return sb.ToString(); + } + private static string ItemContent(string title, string contentType, ChangedMediaTypeBO changedMediaType, bool isRequest) + { + var sb = new StringBuilder(); + sb.Append(ItemContent(title, contentType)) + .Append(new string(' ', 10)) + .Append("Schema: ") + .Append(changedMediaType.IsCompatible() ? "Backward compatible" : "Broken compatibility") + .Append(Environment.NewLine); + if (!changedMediaType.IsCompatible()) + { + sb.Append(Incompatibilities(changedMediaType.Schema)); + } + return sb.ToString(); + } + private static string Incompatibilities(ChangedSchemaBO schema) + { + return Incompatibilities("", schema); + } + private static string Incompatibilities(string propName, ChangedSchemaBO schema) + { + var sb = new StringBuilder(); + if (schema.Items != null) + { + sb.Append(Items(propName, schema.Items)); + } + if (schema.IsCoreChanged().DiffResult == DiffResultEnum.Incompatible && schema.IsChangedType) + { + var type = schema.OldSchema.GetSchemaType() + " -> " + schema.OldSchema.GetSchemaType(); + sb.Append(Property(propName, "Changed property type", type)); + } + var prefix = propName.IsNullOrEmpty() ? "" : propName + "."; + sb.Append( + Properties(prefix, "Missing property", schema.MissingProperties, schema.Context)); + foreach (var (name, value) in schema.ChangedProperties) + { + sb.Append(Incompatibilities(prefix + name, value)); + } + return sb.ToString(); + } + private static string Items(string propName, ChangedSchemaBO schema) + { + var sb = new StringBuilder(); + sb.Append(Incompatibilities(propName + "[]", schema)); + return sb.ToString(); + } + private static string Properties(string propPrefix, string title, Dictionary properties, DiffContextBO context) + { + var sb = new StringBuilder(); + if (properties != null) + { + foreach (var (key, value) in properties) + { + sb.Append(Property(propPrefix + key, title, Resolve(value))); + } + } + return sb.ToString(); + } + private static OpenApiSchema Resolve(OpenApiSchema schema) + { + return RefPointer.ResolveRef(_diff.NewSpecOpenApi.Components, schema, schema.Reference?.ReferenceV3); + } + private static string Property(string name, string title, OpenApiSchema schema) + { + return Property(name, title, Type(schema)); + } + private static string Property(string name, string title, string type) + { + return $"{new string(' ', 10)}{title}: {name} {type}\n"; + } + private static string Type(OpenApiSchema schema) + { + var result = "object"; + if (schema.GetSchemaType() == SchemaTypeEnum.ArraySchema) + { + result = "array"; + } + else if (schema.Type != null) + { + result = schema.Type; + } + return result; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs b/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs new file mode 100644 index 000000000..40967ad9a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using RazorLight; + +namespace Microsoft.OpenApi.Diff.Output.Html +{ + public class HtmlRender : BaseRenderer, IHtmlRender + { + private readonly string _title; + private readonly RazorLightEngine _engine; + + public HtmlRender() + { + _engine = new RazorLightEngineBuilder() + .UseEmbeddedResourcesProject(typeof(HtmlRender)) + .UseMemoryCachingProvider() + .Build(); + } + + public HtmlRender(string title) : this() + { + _title = title; + } + + public async Task Render(ChangedOpenApiBO diff) + { + var model = !_title.IsNullOrEmpty() ? GetRenderModel(diff, _title) : GetRenderModel(diff); + return await _engine.CompileRenderAsync("Views.Index", model); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs b/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs new file mode 100644 index 000000000..78c04b29a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output.Html +{ + public interface IHtmlRender : IRender + { + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml new file mode 100644 index 000000000..2379913d4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml @@ -0,0 +1,31 @@ +@using Microsoft.OpenApi.Diff.Enums +@using Microsoft.OpenApi.Diff.Extensions +@using RazorLight +@inherits TemplatePage> +@model List + + @foreach (var changeViewModel in Model) + { +
@string.Join(" -> ", changeViewModel.Path)
+
+
+ @foreach (var singleChange in changeViewModel.Changes) + { +
@singleChange.ElementType Modification
+ if (singleChange.ChangeType == TypeEnum.Changed) + { +
+ @singleChange.FieldName changed from + @(!singleChange.OldValue.IsNullOrEmpty() ? singleChange.OldValue : " ") to + @(!singleChange.NewValue.IsNullOrEmpty() ? singleChange.NewValue : " ") +
+ } + else + { +
@singleChange.ChangeType @singleChange.FieldName
+ } + } +
+
+ } + diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml new file mode 100644 index 000000000..eb2d398d0 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml @@ -0,0 +1,19 @@ +@model List + + + + @foreach (var endpoint in Model) + { + + + + } + +
+ + + @endpoint.Method@endpoint.PathUrl + @endpoint.ChangeType.DiffResult + + +
diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml new file mode 100644 index 000000000..cc1f98f7a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml @@ -0,0 +1,185 @@ +@using Microsoft.OpenApi.Diff.Extensions +@using RazorLight +@inherits TemplatePage +@model Microsoft.OpenApi.Diff.Output.RenderViewModel + + + + + @Model.PageTitle + + + + + + + + + + + + + +
+ +
+
+
+ +
+

Changed Endpoints Details

+
+ @foreach (var changedOperation in Model.ChangedEndpoints) + { +

+

+ @changedOperation.Method@changedOperation.PathUrl + @changedOperation.ChangeType.DiffResult +

+

+ +

@changedOperation.Summary

+ + + @if (changedOperation.ChangeType.IsIncompatible()) + { +

Breaking Changes

+
+
+ @{ await IncludeAsync("Views.ChangeDetail", changedOperation.ChangesByType.Where(x => x.ChangeType.IsIncompatible()).ToList()); } +
+ } + + @if (changedOperation.ChangesByType.Any(x => x.ChangeType.IsCompatible())) + { +

Compatible Changes

+
+
+ @{ await IncludeAsync("Views.ChangeDetail", changedOperation.ChangesByType.Where(x => x.ChangeType.IsCompatible()).ToList()); } +
+ } + } +
+
+ +
+
+
+ Created: @Model.CreatedDate + + GitHub OpenAPI Diff + +
+
+ + + diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml new file mode 100644 index 000000000..25cb323d1 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml @@ -0,0 +1,14 @@ +@model List + + + + @foreach (var endpoint in Model) + { + + + + } + +
+ @endpoint.Method@endpoint.PathUrl +
diff --git a/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs b/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs new file mode 100644 index 000000000..39d22238a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output +{ + public interface IConsoleRender : IRender + { + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/Output/IRender.cs b/src/Microsoft.OpenApi.Diff/Output/IRender.cs new file mode 100644 index 000000000..0fa12b76e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/IRender.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Output +{ + public interface IRender + { + Task Render(ChangedOpenApiBO diff); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs b/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs new file mode 100644 index 000000000..57f73c837 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output.Markdown +{ + public interface IMarkdownRender : IRender + { + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs b/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs new file mode 100644 index 000000000..185cb3541 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Output.Markdown +{ + public class MarkdownRender : BaseRenderer, IMarkdownRender + { + private readonly ILogger _logger; + private readonly string _title; + + public MarkdownRender(ILogger logger) + { + _logger = logger; + } + + public MarkdownRender(string title, ILogger logger) : this(logger) + { + _title = title; + } + + public Task Render(ChangedOpenApiBO diff) + { + var model = !_title.IsNullOrEmpty() ? GetRenderModel(diff, _title) : GetRenderModel(diff); + return Task.FromResult(GetIndex(model)); + } + + + private string GetIndex(RenderViewModel model) + { + return $"# {model.Name}\n" + + $"Compared Specs: **{model.OldSpecIdentifier}** - **{model.NewSpecIdentifier}**\n" + + $"Report Result: " + + $"\"{model.ChangeType.DiffResult}\"\n" + + $"## Added Endpoints\n" + + $"{GetOperationOverview(model.NewEndpoints)}\n" + + $"## Removed Endpoints\n" + + $"{GetOperationOverview(model.MissingEndpoints)}\n" + + $"## Deprecated Endpoints\n" + + $"{GetOperationOverview(model.DeprecatedEndpoints)}\n" + + $"## Changed Endpoints\n" + + $"{GetChangedOperationOverview(model.ChangedEndpoints)}"; + } + + private string GetOperationOverview(IEnumerable endpoints) + { + var returnString = string.Empty; + foreach (var endpoint in endpoints) + { + returnString += $"\"{endpoint.Method}\" **{endpoint.PathUrl}**\n"; + } + return returnString; + } + + private string GetColorForDiffResult(DiffResultEnum diffResult) + { + switch (diffResult) + { + case DiffResultEnum.NoChanges: + return "grey"; + case DiffResultEnum.Metadata: + return "blue"; + case DiffResultEnum.Compatible: + return "green"; + case DiffResultEnum.Unknown: + return "orange"; + case DiffResultEnum.Incompatible: + return "red"; + default: + throw new ArgumentOutOfRangeException(nameof(diffResult), diffResult, null); + } + } + + private string GetChangedOperationOverview(IEnumerable endpoints) + { + var returnString = string.Empty; + foreach (var endpoint in endpoints) + { + returnString += $"
\n" + + $" " + + $"\"{endpoint.Method}\" " + + $"{endpoint.PathUrl} " + + $"\"{endpoint.ChangeType.DiffResult}\"" + + $"\n" + + $" \n"; + + if (endpoint.ChangeType.IsIncompatible()) + { + returnString += $">
\n" + + $"> Breaking Changes\n" + + $"> \n" + + $"{GetChangeDetails(endpoint.ChangesByType.Where(x => x.ChangeType.IsIncompatible()))}" + + $">
\n" + + $"> \n"; + } + + if (endpoint.ChangesByType.Any(x => x.ChangeType.IsCompatible())) + { + returnString += $">
\n" + + $"> Compatible Changes\n" + + $"> \n" + + $"{GetChangeDetails(endpoint.ChangesByType.Where(x => x.ChangeType.IsCompatible()))}" + + $">
\n" + + $"> \n"; + } + + returnString += $"
\n\n"; + } + return returnString; + } + + private string GetChangeDetails(IEnumerable changes) + { + var returnString = string.Empty; + foreach (var change in changes) + { + returnString += $"> - **{string.Join(" - ", change.Path)}**\n"; + + foreach (var singleChange in change.Changes) + { + returnString += $"> - {singleChange.ElementType} Modification\n"; + + if (singleChange.ChangeType == TypeEnum.Changed) + { + returnString += $"> - `{singleChange.ElementType}` changed from " + + $"`{(!singleChange.OldValue.IsNullOrEmpty() ? singleChange.OldValue : " ")}` to " + + $"`{(!singleChange.NewValue.IsNullOrEmpty() ? singleChange.NewValue : " ")}`\n"; + } + else + { + returnString += $"> - {singleChange.ChangeType} `{singleChange.FieldName}`\n"; + } + } + } + returnString += $"> \n"; + return returnString; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs b/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs new file mode 100644 index 000000000..8490ce337 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Output +{ + public class RenderViewModel + { + public DiffResultBO ChangeType { get; set; } + public DateTime CreatedDate => DateTime.Now; + public string Author { get; set; } + public string Description { get; set; } + public string LogoUrl { get; set; } + public string Name { get; set; } + public string PageTitle { get; set; } + public IReadOnlyCollection NewEndpoints { get; set; } + public IReadOnlyCollection MissingEndpoints { get; set; } + public IReadOnlyCollection DeprecatedEndpoints { get; set; } + public IReadOnlyCollection ChangedEndpoints { get; set; } + public string OldSpecIdentifier { get; set; } + public string NewSpecIdentifier { get; set; } + } + + public class ChangedEndpointViewModel + { + public string PathUrl { get; set; } + public OperationType Method { get; set; } + public string Summary { get; set; } + public DiffResultBO ChangeType { get; set; } + public List ChangesByType { get; set; } + } + + public class ChangeViewModel + { + public List Path { get; set; } + public DiffResultBO ChangeType { get; set; } + public List Changes { get; set; } + } + + public class SingleChangeViewModel + { + public ChangedElementTypeEnum ElementType { get; set; } + public TypeEnum ChangeType { get; set; } + public string FieldName { get; set; } + public string OldValue { get; set; } + public string NewValue { get; set; } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs b/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs new file mode 100644 index 000000000..0b0363670 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public static class ChangedUtils + { + public static bool IsUnchanged(ChangedBO changed) + { + return changed == null || changed.IsUnchanged(); + } + + public static bool IsCompatible(ChangedBO changed) + { + return changed == null || changed.IsCompatible(); + } + + public static T IsChanged(T changed) + where T : ChangedBO + { + return IsUnchanged(changed) ? null : changed; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/Copy.cs b/src/Microsoft.OpenApi.Diff/Utils/Copy.cs new file mode 100644 index 000000000..69db83c9f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/Copy.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public static class Copy + { + public static Dictionary CopyDictionary(this Dictionary dict) + { + return dict == null ? new Dictionary() : new Dictionary(dict); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs b/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs new file mode 100644 index 000000000..8f5cf1a15 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public class EndpointUtils + { + public static List ConvertToEndpoints(string pathUrl, Dictionary dict) + where T : EndpointBO, new() + { + var endpoints = new List(); + if (dict == null) return endpoints; + foreach (var (key, value) in dict) + { + var endpoint = ConvertToEndpoint(pathUrl, key, value); + endpoints.Add(endpoint); + } + return endpoints; + } + + public static T ConvertToEndpoint(string pathUrl, OperationType httpMethod, OpenApiOperation operation) + where T : EndpointBO, new() + { + var endpoint = new T + { + PathUrl = pathUrl, + Method = httpMethod, + Summary = operation.Summary, + Operation = operation + }; + return endpoint; + } + + public static List ConvertToEndpointList(Dictionary dict) + where T : EndpointBO, new() + { + var endpoints = new List(); + if (dict == null) return endpoints; + + foreach (var (key, value) in dict) + { + var operationMap = value.Operations; + foreach (var (operationType, openApiOperation) in operationMap) + { + var endpoint = new T + { + PathUrl = key, + Method = operationType, + Summary = openApiOperation.Summary, + Path = value, + Operation = openApiOperation + }; + endpoints.Add(endpoint); + } + } + return endpoints; + } + } + +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs b/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs new file mode 100644 index 000000000..5cc2d968d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public class RefPointer + { + public const string BaseRef = "#/components/"; + private readonly RefTypeEnum _refType; + + public RefPointer(RefTypeEnum refType) + { + _refType = refType; + } + + public T ResolveRef(OpenApiComponents components, T t, string reference) + { + if (reference != null) + { + var refName = GetRefName(reference); + var maps = GetMap(components); + maps.TryGetValue(refName, out var result); + if (result == null) + { + var caseInsensitiveDictionary = new Dictionary(maps, StringComparer.OrdinalIgnoreCase); + if (caseInsensitiveDictionary.TryGetValue(refName, out var insensitiveValue)) + throw new Exception($"Reference case sensitive error. {refName} is not equal to {caseInsensitiveDictionary.First(x => x.Value.Equals(insensitiveValue)).Key}"); + + throw new AggregateException($"ref '{reference}' doesn't exist."); + } + return result; + } + return t; + } + + private Dictionary GetMap(OpenApiComponents components) + { + switch (_refType) + { + case RefTypeEnum.RequestBodies: + return (Dictionary)components.RequestBodies; + case RefTypeEnum.Responses: + return (Dictionary)components.Responses; + case RefTypeEnum.Parameters: + return (Dictionary)components.Parameters; + case RefTypeEnum.Schemas: + return (Dictionary)components.Schemas; + case RefTypeEnum.Headers: + return (Dictionary)components.Headers; + case RefTypeEnum.SecuritySchemes: + return (Dictionary)components.SecuritySchemes; + default: + throw new ArgumentOutOfRangeException("Not mapped for refType: " + _refType); + } + } + + public string GetRefName(string reference) + { + if (reference == null) + { + return null; + } + if (_refType == RefTypeEnum.SecuritySchemes) + { + return reference; + } + + var baseRef = GetBaseRefForType(_refType.GetDisplayName()); + if (!reference.StartsWith(baseRef, StringComparison.CurrentCultureIgnoreCase)) + { + throw new AggregateException("Invalid ref: " + reference); + } + return reference.Substring(baseRef.Length); + } + + private static string GetBaseRefForType(string type) + { + return $"{BaseRef}{type}/"; + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs b/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs new file mode 100644 index 000000000..e56501490 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Tests +{ + public interface ITestUtils + { + void AssertOpenAPIAreEquals(string oldSpec, string newSpec); + void AssertOpenAPIChangedEndpoints(string oldSpec, string newSpec); + void AssertOpenAPIBackwardCompatible(string oldSpec, string newSpec, bool isDiff); + void AssertOpenAPIBackwardIncompatible(string oldSpec, string newSpec); + IOpenAPICompare GetOpenAPICompare(); + IEnumerable GetChangesOfType(ChangedOpenApiBO changedOpenAPI, DiffResultEnum changeType); + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj b/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj new file mode 100644 index 000000000..0dddd40b7 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj @@ -0,0 +1,161 @@ + + + + netcoreapp3.1 + + false + + + + + true + false + false + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml new file mode 100644 index 000000000..e5e93653c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml @@ -0,0 +1,71 @@ +openapi: 3.0.0 +# Added by API Auto Mocking Plugin +servers: + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/anshul10s/pet-store/1.0.0 +info: + description: | + This is a sample Petstore server. You can find + out more about Swagger at + [http://swagger.io](http://swagger.io) or on + [irc.freenode.net, #swagger](http://swagger.io/irc/). + version: "1.0.0" + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities. Available, reserved, sold is must in respone. Other keys can still be there. + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/Inventory' + x-key-property : + $ref: '#/components/schemas/InvStatus' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + InvStatus: + type: string + enum: + - available + - reserved + - sold + Inventory: + type: object + properties: + id: + type: string + details: + type: count + extra_info: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml new file mode 100644 index 000000000..5b2396990 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +# Added by API Auto Mocking Plugin +servers: + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/anshul10s/pet-store/1.0.0 +info: + description: | + This is a sample Petstore server. You can find + out more about Swagger at + [http://swagger.io](http://swagger.io) or on + [irc.freenode.net, #swagger](http://swagger.io/irc/). + version: "1.0.0" + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities. Available, reserved, sold is must in respone. Other keys can still be there. + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/Inventory' + x-key-property : + $ref: '#/components/schemas/InvStatus' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + InvStatus: + type: string + enum: + - available + - reserved + - sold + Inventory: + type: object + properties: + id: + type: string + details: + type: count \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml new file mode 100644 index 000000000..2d1cc4060 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + properties: + pet_color: + type: string + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: string + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml new file mode 100644 index 000000000..2e9702cee --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml @@ -0,0 +1,127 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + type: string + Cat: + description: Cat class + type: object + required: + - pet_type + properties: + pet_type: + type: string + name: + type: string + pet_color: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string + pet_color: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml new file mode 100644 index 000000000..c5fd84803 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml @@ -0,0 +1,126 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: string + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml new file mode 100644 index 000000000..38fcbb1e8 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + properties: + pet_color: + type: string + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: number + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml new file mode 100644 index 000000000..4e8935e5e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Dog' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + # MyResponseType: + # oneOf: + # - $ref: '#/components/schemas/Cat' + # - $ref: '#/components/schemas/Dog' + # - $ref: '#/components/schemas/Lizard' + # discriminator: + # propertyName: pet_type + # mapping: + # dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml new file mode 100644 index 000000000..2017d5fc5 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml @@ -0,0 +1,132 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Cat" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + # MyResponseType: + # oneOf: + # - $ref: '#/components/schemas/Cat' + # - $ref: '#/components/schemas/Dog' + # - $ref: '#/components/schemas/Lizard' + # discriminator: + # propertyName: pet_type + # mapping: + # dog: '#/components/schemas/Dog' diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml new file mode 100644 index 000000000..39ad0ae16 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml @@ -0,0 +1,134 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 16 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml new file mode 100644 index 000000000..621dc0625 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml @@ -0,0 +1,152 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 24 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml new file mode 100644 index 000000000..59c400ced --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml @@ -0,0 +1,168 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + get: + tags: + - pet + summary: Finds Pets by name + description: name can be provided for the pet + operationId: getPet + parameters: + - name: name + in: query + description: name that need to be considered for filter + required: true + schema: + type: string + responses: + "200": + description: successful operation + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 36 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml new file mode 100644 index 000000000..7da59aa85 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml @@ -0,0 +1,157 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + deprecated: true + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml new file mode 100644 index 000000000..69f6c75ef --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 16 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml new file mode 100644 index 000000000..d3c5b5011 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml @@ -0,0 +1,123 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml new file mode 100644 index 000000000..bb23d7931 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml new file mode 100644 index 000000000..f51b5c5ca --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml @@ -0,0 +1,32 @@ +--- +openapi: "3.0.1" +info: + title: "Test title" + description: "This is a test metadata" + termsOfService: "http://test.com" + contact: + name: "Mark Snijder" + url: "marksnijder.nl" + email: "snijderd@gmail.com" + license: + name: "To be decided" + url: "http://test.com" + version: "version 1.0" +paths: + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: find pet by id + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: response + content: + application/json: {} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml new file mode 100644 index 000000000..53d25e979 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml @@ -0,0 +1,47 @@ +--- +openapi: "3.0.1" +info: + title: "Test title" + description: "This is a test metadata" + termsOfService: "http://test.com" + contact: + name: "Mark Snijder" + url: "marksnijder.nl" + email: "snijderd@gmail.com" + license: + name: "To be decided" + url: "http://test.com" + version: "version 1.0" +paths: + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: find pet by id + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: response + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: "object" + properties: + id: + type: "integer" + format: "int32" + salary: + type: "integer" + format: "int32" + name: + type: "string" \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml new file mode 100644 index 000000000..d05cc9b92 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: integer + content: + application/xml: + schema: + type: integer + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml new file mode 100644 index 000000000..2f0721bf3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit-New: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + content: + application/xml: + schema: + type: integer + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml new file mode 100644 index 000000000..bc8c3c787 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml @@ -0,0 +1,134 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml new file mode 100644 index 000000000..9bc7cbfa4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml @@ -0,0 +1,136 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" + Cat: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml new file mode 100644 index 000000000..a0626e95c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml @@ -0,0 +1,136 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/MyDog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/MyDog' + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + MyDog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml new file mode 100644 index 000000000..c0d88f136 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.1 +info: + title: oneOf test for issue 29 + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /state: + post: + operationId: update + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/A' + - $ref: '#/components/schemas/B' + discriminator: + propertyName: realtype + mapping: + a-type: '#/components/schemas/A' + b-type: '#/components/schemas/B' + required: true + responses: + '201': + description: OK +components: + schemas: + A: + type: object + properties: + realtype: + type: string + othertype: + type: string + message: + type: string + B: + type: object + properties: + realtype: + type: string + othertype: + type: string + description: + type: string + code: + type: integer + format: int32 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml new file mode 100644 index 000000000..c479c17d6 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.1 +info: + title: oneOf test for issue 29 + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /state: + post: + operationId: update + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/A' + - $ref: '#/components/schemas/B' + discriminator: + propertyName: othertype + mapping: + a-type: '#/components/schemas/A' + b-type: '#/components/schemas/B' + required: true + responses: + '201': + description: OK +components: + schemas: + A: + type: object + properties: + realtype: + type: string + othertype: + type: string + message: + type: string + B: + type: object + properties: + realtype: + type: string + othertype: + type: string + description: + type: string + code: + type: integer + format: int32 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml new file mode 100644 index 000000000..97cc8e9b9 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml new file mode 100644 index 000000000..97cc8e9b9 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml new file mode 100644 index 000000000..a4e38e85d --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml @@ -0,0 +1,183 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 2.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + parameters: + - name: tags + in: query + description: add new query param demo + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml new file mode 100644 index 000000000..28128a820 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml new file mode 100644 index 000000000..9eaf5a83f --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId2}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId2 + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml new file mode 100644 index 000000000..59a315cd0 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input + /pet/{petId2}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId2 + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml new file mode 100644 index 000000000..a4e506262 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml @@ -0,0 +1,670 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Muliple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value >= 1 and <= 10. Other + values will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 10 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with positive integer value. Negative + or non-integer values will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be updated + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml new file mode 100644 index 000000000..74c5525ab --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml @@ -0,0 +1,686 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + parameters: + - name: tags + in: query + description: add new query param demo + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Muliple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image for pet + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: 'ID of pet to update, default false' + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value >= 1 and <= 10. Other + values will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 10 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with positive integer value. Negative + or non-integer values will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit-New: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: integer + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + deprecated: true + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be updated + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + userStatus: + type: integer + format: int32 + description: User Status + newUserFeild: + type: integer + format: int32 + description: a new user feild demo + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml new file mode 100644 index 000000000..eddf6ce38 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml @@ -0,0 +1,167 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: {} +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml new file mode 100644 index 000000000..6a05ab0ad --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + message2: + type: string + details: + type: array + items: + $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml new file mode 100644 index 000000000..83d78256e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + details: + type: array + items: + $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml new file mode 100644 index 000000000..c118a2ada --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + message2: + type: string + details: + allOf: + - $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml new file mode 100644 index 000000000..5b8a7c0d4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + details: + allOf: + - $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml new file mode 100644 index 000000000..0a31c8c81 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml @@ -0,0 +1,162 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet1: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pet2: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet3: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + $ref: '#/components/requestBodies/Pet' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: false + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml new file mode 100644 index 000000000..4a902b711 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml @@ -0,0 +1,162 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 2.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet1: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet2: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet3: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + $ref: '#/components/requestBodies/Pet' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml new file mode 100644 index 000000000..d08c9f1d8 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml @@ -0,0 +1,173 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + requestBody: + $ref: "#/components/requestBodies/Pet" + get: + tags: + - pet + summary: Finds Pets by name + description: name can be provided for the pet + operationId: getPet + parameters: + - name: name + in: query + description: name that need to be considered for filter + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml new file mode 100644 index 000000000..786df4279 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml @@ -0,0 +1,240 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml new file mode 100644 index 000000000..2758c2d1b --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml @@ -0,0 +1,266 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - tenant: [] + user: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth2: + - 'write:pets' + - 'read:pets' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth3: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth3: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog3' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + petstore_auth2: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml new file mode 100644 index 000000000..60b20d930 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml @@ -0,0 +1,241 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + - unknown: [] +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs b/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs new file mode 100644 index 000000000..0bb3f2590 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests +{ + public class TestUtils : ITestUtils + { + private readonly IOpenAPICompare _openAPICompare; + + public TestUtils(IOpenAPICompare openAPICompare) + { + _openAPICompare = openAPICompare; + } + + public void AssertOpenAPIAreEquals(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.Empty(changedOpenAPI.ChangedOperations); + } + + public void AssertOpenAPIChangedEndpoints(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.NotEmpty(changedOpenAPI.ChangedOperations); + } + + public void AssertOpenAPIBackwardCompatible(string oldSpec, string newSpec, bool isDiff) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.True(changedOpenAPI.IsCompatible()); + } + + public void AssertOpenAPIBackwardIncompatible(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.True(changedOpenAPI.IsIncompatible()); + Assert.NotEmpty(GetChangesOfType(changedOpenAPI, DiffResultEnum.Incompatible)); + } + + public IEnumerable GetChangesOfType(ChangedOpenApiBO changedOpenAPI, DiffResultEnum changeType) + { + return changedOpenAPI.GetChangedElements() + .SelectMany(x => x.Change.GetAllChangeInfoFlat(null)) + .Where(y => y.ChangeType.DiffResult == changeType) + .ToList(); + } + + public IOpenAPICompare GetOpenAPICompare() + { + return _openAPICompare; + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs new file mode 100644 index 000000000..9f529ff9e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class AddPropDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/add-prop-1.yaml"; + private const string OpenAPIDoc2 = "Resources/add-prop-2.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs new file mode 100644 index 000000000..af4f25bf3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class AllOfDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/allOf_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/allOf_diff_2.yaml"; + private const string OpenAPIDoc3 = "Resources/allOf_diff_3.yaml"; + private const string OpenAPIDoc4 = "Resources/allOf_diff_4.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffSameWithAllOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void TestDiffDifferent1() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc3); + } + + [Fact] + public void TestDiffDifferent2() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc4); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs new file mode 100644 index 000000000..7a930769e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ArrayDiffTest : BaseTest + { + private const string OpenAPIDoc31 = "Resources/array_diff_1.yaml"; + private const string OpenAPIDoc32 = "Resources/array_diff_2.yaml"; + + [Fact] + public void TestArrayDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc31, OpenAPIDoc32); + } + + [Fact] + public void TestArrayDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc31, OpenAPIDoc31); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs new file mode 100644 index 000000000..4d62b92f3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs @@ -0,0 +1,56 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class BackwardCompatibilityTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/backwardCompatibility/bc_1.yaml"; + private const string OpenAPIDoc2 = "Resources/backwardCompatibility/bc_2.yaml"; + private const string OpenAPIDoc3 = "Resources/backwardCompatibility/bc_3.yaml"; + private const string OpenAPIDoc4 = "Resources/backwardCompatibility/bc_4.yaml"; + private const string OpenAPIDoc5 = "Resources/backwardCompatibility/bc_5.yaml"; + + [Fact] + public void TestNoChange() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc1, false); + } + + [Fact] + public void TestAPIAdded() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc2, true); + } + + [Fact] + public void TestAPIMissing() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc2, OpenAPIDoc1); + } + + [Fact] + public void TestAPIChangedOperationAdded() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc2, OpenAPIDoc3, true); + } + + [Fact] + public void TestAPIChangedOperationMissing() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc3, OpenAPIDoc2); + } + + [Fact] + public void TestAPIOperationChanged() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc2, OpenAPIDoc4, true); + } + + [Fact] + public void TestAPIReadWriteOnlyPropertiesChanged() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc5, true); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs new file mode 100644 index 000000000..f464b724e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ContentDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/content_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/content_diff_2.yaml"; + + [Fact] + public void TestContentDiffWithOneEmptyMediaType() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + Assert.True(changedOpenAPI.IsIncompatible()); + } + + [Fact] + public void TestContentDiffWithEmptyMediaTypes() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc1); + Assert.True(changedOpenAPI.IsUnchanged()); + } + + [Fact] + public void TestSameContentDiff() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc2, OpenAPIDoc2); + Assert.True(changedOpenAPI.IsUnchanged()); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs new file mode 100644 index 000000000..9268bc6ff --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs @@ -0,0 +1,47 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class OneOfDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/oneOf_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/oneOf_diff_2.yaml"; + private const string OpenAPIDoc3 = "Resources/oneOf_diff_3.yaml"; + private const string OpenAPIDoc4 = "Resources/composed_schema_1.yaml"; + private const string OpenAPIDoc5 = "Resources/composed_schema_2.yaml"; + private const string OpenAPIDoc6 = "Resources/oneOf_discriminator-changed_1.yaml"; + private const string OpenAPIDoc7 = "Resources/oneOf_discriminator-changed_2.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffDifferentMapping() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void testDiffSameWithOneOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc2, OpenAPIDoc3); + } + + [Fact] + public void TestComposedSchema() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc4, OpenAPIDoc5); + } + + [Fact] + public void TestOneOfDiscriminatorChanged() + { + // The oneOf 'discriminator' changed: 'realtype' -> 'othertype': + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc6, OpenAPIDoc7); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs new file mode 100644 index 000000000..32f72121c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.Output.Html; +using Microsoft.OpenApi.Diff.Output.Markdown; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class OpenAPIDiffTest : BaseTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public OpenAPIDiffTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + private const string OpenAPIDoc1 = "Resources/petstore_v2_1.yaml"; + private const string OpenAPIDoc2 = "Resources/petstore_v2_2.yaml"; + private const string OpenAPIEmptyDoc = "Resources/petstore_v2_empty.yaml"; + + [Fact] + public void TestEqual() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc2, OpenAPIDoc2); + } + + [Fact] + public async void TestNewAPI() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIEmptyDoc, OpenAPIDoc2); + var newEndpoints = changedOpenAPI.NewEndpoints; + var missingEndpoints = changedOpenAPI.MissingEndpoints; + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + + try + { + File.WriteAllText("testNewAPI.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.NotEmpty(newEndpoints); + Assert.Empty(missingEndpoints); + Assert.Empty(changedEndPoints); + } + + [Fact] + public async Task TestDeprecatedAPI() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIEmptyDoc); + var newEndpoints = changedOpenAPI.NewEndpoints; + var missingEndpoints = changedOpenAPI.MissingEndpoints; + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + + try + { + File.WriteAllText("testDeprecatedAPI.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.Empty(newEndpoints); + Assert.NotEmpty(missingEndpoints); + Assert.Empty(changedEndPoints); + } + + [Fact] + public async Task TestDiff() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.NotEmpty(changedEndPoints); + } + + [Fact] + public async Task TestDiffAndMarkdown() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + var logger = _testOutputHelper.BuildLoggerFor(); + var render = await new MarkdownRender(logger).Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.md", render); + + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + } + + [Fact] + public async Task TestDiffAndHtml() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + //var incompatibleChanges = TestUtils.GetChangesOfType(changedOpenAPI, DiffResultEnum.Incompatible); + var render = await new HtmlRender().Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.html", render); + + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs new file mode 100644 index 000000000..d48686fb3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ParameterDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/parameters_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/parameters_diff_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs new file mode 100644 index 000000000..2fcaf7537 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class PathDiffTest : BaseTest + { + private const string OpenAPIPath1 = "Resources/path_1.yaml"; + private const string OpenAPIPath2 = "Resources/path_2.yaml"; + private const string OpenAPIPath3 = "Resources/path_3.yaml"; + + [Fact] + public void TestEqual() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIPath1, OpenAPIPath2); + } + + [Fact] + public void TestMultiplePathWithSameSignature() + { + Assert.Throws(() => TestUtils.AssertOpenAPIAreEquals(OpenAPIPath3, OpenAPIPath3)); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs new file mode 100644 index 000000000..25d0595e0 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class RecursiveSchemaTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/recursive_model_1.yaml"; + private const string OpenAPIDoc2 = "Resources/recursive_model_2.yaml"; + private const string OpenAPIDoc3 = "Resources/recursive_model_3.yaml"; + private const string OpenAPIDoc4 = "Resources/recursive_model_4.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffSameWithAllOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc3, OpenAPIDoc3); + } + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void TestDiffDifferentWithAllOf() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc3, OpenAPIDoc4); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs new file mode 100644 index 000000000..75becbfd1 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs @@ -0,0 +1,16 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ReferenceDiffCacheTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/schema_diff_cache_1.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs new file mode 100644 index 000000000..c41815cbd --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class RequestDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/request_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/request_diff_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs new file mode 100644 index 000000000..6bf8fa9e4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ResponseHeaderDiffTest : BaseTest + { + private const string OpenapiDoc1 = "Resources/header_1.yaml"; + private const string OpenapiDoc2 = "Resources/header_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc1, OpenapiDoc2); + + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.NotEmpty(changedOpenAPI.ChangedOperations); + + var changedResponses = changedOpenAPI.ChangedOperations.FirstOrDefault()?.APIResponses.Changed; + + Assert.NotNull(changedResponses); + Assert.NotEmpty(changedResponses); + Assert.True(changedResponses.ContainsKey("200")); + + var changedHeaders = changedResponses["200"].Headers; + Assert.True(changedHeaders.IsDifferent()); + Assert.Single(changedHeaders.Changed); + Assert.Single(changedHeaders.Increased); + Assert.Single(changedHeaders.Missing); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs new file mode 100644 index 000000000..b6b127090 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using Microsoft.OpenApi.Diff.Tests._Base; +using Microsoft.OpenApi.Readers; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class SecurityDiffTest : BaseTest + { + private const string OpenapiDoc1 = "Resources/security_diff_1.yaml"; + private const string OpenapiDoc2 = "Resources/security_diff_2.yaml"; + private const string OpenapiDoc3 = "Resources/security_diff_3.yaml"; + + [Fact] + public void TestDiffDifferent() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc1, OpenapiDoc2); + Assert.Equal(3, changedOpenAPI.ChangedOperations.Count); + + var changedOperation1 = changedOpenAPI + .ChangedOperations + .FirstOrDefault(x => x.PathUrl.Equals("/pet/{petId}")); + Assert.NotNull(changedOperation1); + Assert.False(changedOperation1.IsCompatible()); + + var changedSecurityRequirements1 = changedOperation1.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements1); + Assert.False(changedSecurityRequirements1.IsCompatible()); + Assert.Single(changedSecurityRequirements1.Increased); + Assert.Single(changedSecurityRequirements1.Changed); + + var changedSecurityRequirement1 = changedSecurityRequirements1.Changed.FirstOrDefault(); + + Assert.NotNull(changedSecurityRequirement1); + Assert.Single(changedSecurityRequirement1.Changed); + + var changedScopes1 = + changedSecurityRequirement1.Changed.First().ChangedScopes; + Assert.NotNull(changedScopes1); + Assert.Single(changedScopes1.Increased); + Assert.Equal("read:pets", changedScopes1.Increased.First()); + + var changedOperation2 = + changedOpenAPI + .ChangedOperations.FirstOrDefault(x => x.PathUrl == "/pet3"); + Assert.NotNull(changedOperation2); + Assert.False(changedOperation2.IsCompatible()); + + var changedSecurityRequirements2 = + changedOperation2.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements2); + Assert.False(changedSecurityRequirements2.IsCompatible()); + Assert.Single(changedSecurityRequirements2.Changed); + + var changedSecurityRequirement2 = + changedSecurityRequirements2.Changed.First(); + Assert.Single(changedSecurityRequirement2.Changed); + + var changedImplicitOAuthFlow2 = + changedSecurityRequirement2.Changed.First().OAuthFlows.ImplicitOAuthFlow; + Assert.NotNull(changedImplicitOAuthFlow2); + Assert.True(changedImplicitOAuthFlow2.ChangedAuthorizationUrl); + + var changedOperation3 = + changedOpenAPI + .ChangedOperations + .FirstOrDefault(x => x.PathUrl == "/pet/findByStatus2"); + Assert.NotNull(changedOperation3); + Assert.True(changedOperation3.IsCompatible()); + + var changedSecurityRequirements3 = + changedOperation3.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements3); + Assert.Single(changedSecurityRequirements3.Increased); + + var securityRequirement3 = changedSecurityRequirements3.Increased.First(); + Assert.Single(securityRequirement3); + Assert.All(securityRequirement3.Keys, x => Assert.Equal("petstore_auth", x.Reference.ReferenceV3)); + Assert.Equal(2, securityRequirement3.First().Value.Count); + } + + [Fact] + public void TestWithUnknownSecurityScheme() + { + var settings = new OpenApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences + }; + Assert.Throws(() => TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc3, OpenapiDoc3, settings)); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs b/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs new file mode 100644 index 000000000..088822333 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Diff.Compare; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Diff.Tests._Base +{ + public class BaseTest + { + public readonly ITestUtils TestUtils; + public readonly ITestOutputHelper OutputHelper; + + public BaseTest() + { + var services = new ServiceCollection(); + services.AddTransient(); + services.AddTransient(); + services.AddLogging(); + services.RegisterAll(new[] {GetType().Assembly}, ServiceLifetime.Transient); + + var serviceProvider = services.BuildServiceProvider(); + + TestUtils = serviceProvider.GetService(); + OutputHelper = serviceProvider.GetService(); + } + + + } + + public static class ServiceCollectionExtension { + public static void RegisterAll(this IServiceCollection serviceCollection, Assembly[] assemblies, ServiceLifetime lifetime) + { + var typesFromAssemblies = assemblies + .SelectMany(a => a.DefinedTypes.Where(x => x.GetInterfaces().Contains(typeof(T)))); + + foreach (var type in typesFromAssemblies) + serviceCollection.Add(new ServiceDescriptor(typeof(T), type, lifetime)); + } + } +} From f40cac18210f623832322baa8e23cbe95cc152c5 Mon Sep 17 00:00:00 2001 From: Fabian Niederer Date: Mon, 1 Feb 2021 17:55:34 +0100 Subject: [PATCH 2/2] added OpenAPI Diff Co-Authored-By: leowns <1150181+leowns@users.noreply.github.com> --- Microsoft.OpenApi.sln | 14 + .../BusinessObjects/ChangeBO.cs | 34 + .../BusinessObjects/ChangedAPIResponseBO.cs | 59 ++ .../BusinessObjects/ChangedBO.cs | 78 ++ .../BusinessObjects/ChangedContentBO.cs | 55 ++ .../BusinessObjects/ChangedEnumBO.cs | 25 + .../BusinessObjects/ChangedExtensionsBO.cs | 49 ++ .../BusinessObjects/ChangedHeaderBO.cs | 78 ++ .../BusinessObjects/ChangedHeadersBO.cs | 52 ++ .../BusinessObjects/ChangedInfoBO.cs | 32 + .../BusinessObjects/ChangedInfosBO.cs | 11 + .../BusinessObjects/ChangedListBO.cs | 67 ++ .../BusinessObjects/ChangedMaxLengthBO.cs | 47 ++ .../BusinessObjects/ChangedMediaTypeBO.cs | 44 ++ .../BusinessObjects/ChangedMetadataBO.cs | 36 + .../BusinessObjects/ChangedOAuthFlowBO.cs | 62 ++ .../BusinessObjects/ChangedOAuthFlowsBO.cs | 50 ++ .../BusinessObjects/ChangedOneOfSchemaBO.cs | 52 ++ .../BusinessObjects/ChangedOpenApiBO.cs | 64 ++ .../BusinessObjects/ChangedOperationBO.cs | 92 +++ .../BusinessObjects/ChangedParameterBO.cs | 92 +++ .../BusinessObjects/ChangedParametersBO.cs | 54 ++ .../BusinessObjects/ChangedPathBO.cs | 61 ++ .../BusinessObjects/ChangedPathsBO.cs | 53 ++ .../BusinessObjects/ChangedReadOnlyBO.cs | 55 ++ .../BusinessObjects/ChangedRequestBodyBO.cs | 60 ++ .../BusinessObjects/ChangedRequiredBO.cs | 25 + .../BusinessObjects/ChangedResponseBO.cs | 49 ++ .../BusinessObjects/ChangedSchemaBO.cs | 119 +++ .../ChangedSecurityRequirementBO.cs | 50 ++ .../ChangedSecurityRequirementsBO.cs | 54 ++ .../ChangedSecuritySchemeBO.cs | 90 +++ .../ChangedSecuritySchemeScopesBO.cs | 19 + .../BusinessObjects/ChangedWriteOnlyBO.cs | 55 ++ .../BusinessObjects/ComposedChangedBO.cs | 61 ++ .../BusinessObjects/DiffContextBO.cs | 91 +++ .../BusinessObjects/DiffResultBO.cs | 39 + .../BusinessObjects/EndpointBO.cs | 18 + .../Compare/ApiResponseDiff.cs | 46 ++ .../Compare/CacheKey.cs | 48 ++ .../Compare/ContentDiff.cs | 61 ++ .../Compare/ExtensionDiff.cs | 20 + .../Compare/ExtensionsDiff.cs | 93 +++ .../Compare/HeaderDiff.cs | 64 ++ .../Compare/HeadersDiff.cs | 43 ++ .../Compare/IExtensionDiff.cs | 17 + .../Compare/ListDiff.cs | 42 ++ .../Compare/MapKeyDiff.cs | 51 ++ .../Compare/MetadataDiff.cs | 25 + .../Compare/OAuthFlowDiff.cs | 32 + .../Compare/OAuthFlowsDiff.cs | 41 ++ .../Compare/OpenApiDiff.cs | 166 +++++ .../Compare/OperationDiff.cs | 97 +++ .../Compare/ParameterDiff.cs | 65 ++ .../Compare/ParametersDiff.cs | 70 ++ .../Compare/PathDiff.cs | 48 ++ .../Compare/PathsDiff.cs | 77 ++ .../Compare/ReferenceDiffCache.cs | 53 ++ .../Compare/RequestBodyDiff.cs | 87 +++ .../Compare/ResponseDiff.cs | 52 ++ .../Compare/SchemaDiff.cs | 374 ++++++++++ .../SchemaDiffResult/ArraySchemaDiffResult.cs | 37 + .../ComposedSchemaDiffResult.cs | 125 ++++ .../SchemaDiffResult/SchemaDiffResult.cs | 168 +++++ .../Compare/SecurityRequirementDiff.cs | 110 +++ .../Compare/SecurityRequirementsDiff.cs | 98 +++ .../Compare/SecuritySchemeDiff.cs | 118 +++ .../Enums/ChangedElementTypeEnum.cs | 31 + .../Enums/DiffResultEnum.cs | 11 + .../Enums/RefTypeEnum.cs | 12 + .../Enums/SchemaTypeEnum.cs | 9 + src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs | 9 + .../Extensions/IOpenApiPrimitiveExtensions.cs | 17 + .../Extensions/ListExtensions.cs | 32 + .../Extensions/OpenApiSchemaExtensions.cs | 22 + .../Extensions/PathExtensions.cs | 28 + src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs | 12 + .../Microsoft.OpenApi.Diff.csproj | 44 ++ src/Microsoft.OpenApi.Diff/OpenApiCompare.cs | 50 ++ .../Output/BaseRenderer.cs | 70 ++ .../Output/ConsoleRender.cs | 346 +++++++++ .../Output/Html/HtmlRender.cs | 32 + .../Output/Html/IHtmlRender.cs | 6 + .../Output/Html/Views/ChangeDetail.cshtml | 31 + .../Views/ChangedOperationOverview.cshtml | 19 + .../Output/Html/Views/Index.cshtml | 185 +++++ .../Html/Views/OperationOverview.cshtml | 14 + .../Output/IConsoleRender.cs | 6 + src/Microsoft.OpenApi.Diff/Output/IRender.cs | 10 + .../Output/Markdown/IMarkdownRender.cs | 6 + .../Output/Markdown/MarkdownRender.cs | 144 ++++ .../Output/RenderViewModel.cs | 50 ++ .../Utils/ChangedUtils.cs | 23 + src/Microsoft.OpenApi.Diff/Utils/Copy.cs | 12 + .../Utils/EndpointUtils.cs | 61 ++ .../Utils/RefPointer.cs | 85 +++ .../ITestUtils.cs | 16 + .../Microsoft.OpenApi.Diff.Tests.csproj | 161 ++++ .../Resources/add-prop-1.yaml | 71 ++ .../Resources/add-prop-2.yaml | 69 ++ .../Resources/allOf_diff_1.yaml | 129 ++++ .../Resources/allOf_diff_2.yaml | 127 ++++ .../Resources/allOf_diff_3.yaml | 126 ++++ .../Resources/allOf_diff_4.yaml | 129 ++++ .../Resources/array_diff_1.yaml | 133 ++++ .../Resources/array_diff_2.yaml | 132 ++++ .../Resources/backwardCompatibility/bc_1.yaml | 134 ++++ .../Resources/backwardCompatibility/bc_2.yaml | 152 ++++ .../Resources/backwardCompatibility/bc_3.yaml | 168 +++++ .../Resources/backwardCompatibility/bc_4.yaml | 157 ++++ .../Resources/backwardCompatibility/bc_5.yaml | 133 ++++ .../Resources/composed_schema_1.yaml | 123 ++++ .../Resources/composed_schema_2.yaml | 129 ++++ .../Resources/content_diff_1.yaml | 32 + .../Resources/content_diff_2.yaml | 47 ++ .../Resources/header_1.yaml | 131 ++++ .../Resources/header_2.yaml | 131 ++++ .../Resources/oneOf_diff_1.yaml | 134 ++++ .../Resources/oneOf_diff_2.yaml | 136 ++++ .../Resources/oneOf_diff_3.yaml | 136 ++++ .../oneOf_discriminator-changed_1.yaml | 49 ++ .../oneOf_discriminator-changed_2.yaml | 49 ++ .../Resources/parameters_diff.yaml | 185 +++++ .../Resources/parameters_diff_1.yaml | 185 +++++ .../Resources/parameters_diff_2.yaml | 183 +++++ .../Resources/path_1.yaml | 35 + .../Resources/path_2.yaml | 35 + .../Resources/path_3.yaml | 52 ++ .../Resources/petstore_v2_1.yaml | 670 +++++++++++++++++ .../Resources/petstore_v2_2.yaml | 686 ++++++++++++++++++ .../Resources/petstore_v2_empty.yaml | 167 +++++ .../Resources/recursive_model_1.yaml | 30 + .../Resources/recursive_model_2.yaml | 28 + .../Resources/recursive_model_3.yaml | 29 + .../Resources/recursive_model_4.yaml | 27 + .../Resources/request_diff_1.yaml | 162 +++++ .../Resources/request_diff_2.yaml | 162 +++++ .../Resources/schema_diff_cache_1.yaml | 173 +++++ .../Resources/security_diff_1.yaml | 240 ++++++ .../Resources/security_diff_2.yaml | 266 +++++++ .../Resources/security_diff_3.yaml | 241 ++++++ .../Microsoft.OpenApi.Diff.Tests/TestUtils.cs | 60 ++ .../Tests/AddPropDiffTest.cs | 23 + .../Tests/AllOfDiffTest.cs | 37 + .../Tests/ArrayDiffTest.cs | 23 + .../Tests/BackwardCompatibilityTest.cs | 56 ++ .../Tests/ContentDiffTest.cs | 32 + .../Tests/OneOfDiffTest.cs | 47 ++ .../Tests/OpenApiDiffTest.cs | 126 ++++ .../Tests/ParameterDiffTest.cs | 17 + .../Tests/PathDiffTest.cs | 25 + .../Tests/RecursiveSchemaTest.cs | 37 + .../Tests/ReferenceDiffCacheTest.cs | 16 + .../Tests/RequestDiffTest.cs | 17 + .../Tests/ResponseHeaderDiffTest.cs | 34 + .../Tests/SecurityDiffTest.cs | 93 +++ .../_Base/BaseTest.cs | 41 ++ 157 files changed, 12925 insertions(+) create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs create mode 100644 src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs create mode 100644 src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj create mode 100644 src/Microsoft.OpenApi.Diff/OpenApiCompare.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml create mode 100644 src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/IRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs create mode 100644 src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/Copy.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs create mode 100644 src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml create mode 100644 test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs create mode 100644 test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs diff --git a/Microsoft.OpenApi.sln b/Microsoft.OpenApi.sln index e64ff3a24..6c5e85275 100644 --- a/Microsoft.OpenApi.sln +++ b/Microsoft.OpenApi.sln @@ -28,6 +28,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.SmokeTest EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Tool", "src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj", "{254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Diff", "src\Microsoft.OpenApi.Diff\Microsoft.OpenApi.Diff.csproj", "{35F25729-19A7-43BA-AAB5-0859EC524F6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.OpenApi.Diff.Tests", "test\Microsoft.OpenApi.Diff.Tests\Microsoft.OpenApi.Diff.Tests.csproj", "{8491A9E6-4F63-4F55-9DFB-AEFE60602351}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +66,14 @@ Global {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.Build.0 = Release|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35F25729-19A7-43BA-AAB5-0859EC524F6F}.Release|Any CPU.Build.0 = Release|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8491A9E6-4F63-4F55-9DFB-AEFE60602351}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +86,8 @@ Global {1ED3C2C1-E1E7-4925-B4E6-2D969C3F5237} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {AD79B61D-88CF-497C-9ED5-41AE3867C5AC} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE} = {E546B92F-20A8-49C3-8323-4B25BB78F3E1} + {35F25729-19A7-43BA-AAB5-0859EC524F6F} = {E546B92F-20A8-49C3-8323-4B25BB78F3E1} + {8491A9E6-4F63-4F55-9DFB-AEFE60602351} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F171EFC-0DB5-4B10-ABFA-AF48D52CC565} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs new file mode 100644 index 000000000..8d254e02b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangeBO.cs @@ -0,0 +1,34 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangeBO + where T : class + { + public T OldValue { get; } + public T NewValue { get; } + public TypeEnum Type { get; } + + private ChangeBO(T oldValue, T newValue, TypeEnum type) + { + OldValue = oldValue; + NewValue = newValue; + Type = type; + } + + public static ChangeBO Changed(T oldValue, T newValue) + { + return new ChangeBO(oldValue, newValue, TypeEnum.Changed); + } + + public static ChangeBO Added(T newValue) + { + return new ChangeBO(null, newValue, TypeEnum.Added); + } + + public static ChangeBO Removed(T oldValue) + { + return new ChangeBO(oldValue, null, TypeEnum.Removed); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs new file mode 100644 index 000000000..a4a60f96a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedAPIResponseBO.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedAPIResponseBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Response; + + private readonly OpenApiResponses _oldApiResponses; + private readonly OpenApiResponses _newApiResponses; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedAPIResponseBO(OpenApiResponses oldApiResponses, OpenApiResponses newApiResponses, DiffContextBO context) + { + _oldApiResponses = oldApiResponses; + _newApiResponses = newApiResponses; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs new file mode 100644 index 000000000..aa929b204 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedBO.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ChangedBO + { + protected ChangedBO() + { + } + + protected abstract ChangedElementTypeEnum GetElementType(); + protected string GetIdentifier(string identifier) => identifier ?? GetElementType().GetDisplayName(); + + public abstract DiffResultBO IsChanged(); + + public virtual DiffResultBO IsCoreChanged() => IsChanged(); + + protected abstract List GetCoreChanges(); + + public ChangedInfosBO GetCoreChangeInfo(string identifier, List parentPath = null) + { + var isChanged = IsCoreChanged(); + var newPath = new List(); + + if (!parentPath.IsNullOrEmpty()) + newPath = new List(parentPath); + + newPath.Add(GetIdentifier(identifier)); + + var result = new ChangedInfosBO + { + Path = newPath, + ChangeType = isChanged + }; + + if (isChanged.IsUnchanged()) + return result; + + result.Changes = GetCoreChanges(); + return result; + } + + public virtual List GetAllChangeInfoFlat(string identifier, List parentPath = null) + { + return new List + { + GetCoreChangeInfo(identifier, parentPath) + }; + } + + public static DiffResultBO Result(ChangedBO changed) + { + return changed?.IsChanged() ?? new DiffResultBO(DiffResultEnum.NoChanges); + } + public bool IsCompatible() + { + return IsChanged().IsCompatible(); + } + + public bool IsIncompatible() + { + return IsChanged().IsIncompatible(); + } + + public bool IsUnchanged() + { + return IsChanged().IsUnchanged(); + } + + public bool IsDifferent() + { + return IsChanged().IsDifferent(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs new file mode 100644 index 000000000..3ddee041d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedContentBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedContentBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Content; + + private readonly Dictionary _oldContent; + private readonly Dictionary _newContent; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedContentBO(Dictionary oldContent, Dictionary newContent, DiffContextBO context) + { + _oldContent = oldContent; + _newContent = newContent; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest && Missing.IsNullOrEmpty() || _context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs new file mode 100644 index 000000000..c5cedb49c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedEnumBO.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedEnumBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Enum; + + public ChangedEnumBO(IList oldValue, IList newValue, DiffContextBO context) : base(oldValue, newValue, context) + { + } + + public override DiffResultBO IsItemsChanged() + { + if (Context.IsRequest && Missing.IsNullOrEmpty() + || Context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs new file mode 100644 index 000000000..ebed603e4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedExtensionsBO.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedExtensionsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Extension; + + private readonly Dictionary _oldExtensions; + private readonly Dictionary _newExtensions; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedExtensionsBO(Dictionary oldExtensions, Dictionary newExtensions, DiffContextBO context) + { + _oldExtensions = oldExtensions; + _newExtensions = newExtensions; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>() + .Concat(Increased.Select(x => (x.Key, (ChangedBO)x.Value))) + .Concat(Missing.Select(x => (x.Key, (ChangedBO)x.Value))) + .Concat(Changed.Select(x => (x.Key, (ChangedBO)x.Value))) + .Where(x => x.Item2 != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs new file mode 100644 index 000000000..1a2e10bb0 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeaderBO.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedHeaderBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Header; + + public OpenApiHeader OldHeader { get; } + public OpenApiHeader NewHeader { get; } + private readonly DiffContextBO _context; + + public bool Required { get; set; } + public bool Deprecated { get; set; } + public bool Style { get; set; } + public bool Explode { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedSchemaBO Schema { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedHeaderBO(OpenApiHeader oldHeader, OpenApiHeader newHeader, DiffContextBO context) + { + OldHeader = oldHeader; + NewHeader = newHeader; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Description", Description), + ("Schema", Schema), + ("Content", Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!Required && !Deprecated && !Style && !Explode) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!Required && !Style && !Explode) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (Required) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", OldHeader?.Required.ToString(), NewHeader?.Required.ToString())); + + if (Deprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldHeader?.Deprecated.ToString(), NewHeader?.Deprecated.ToString())); + + if (Style) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Style", OldHeader?.Style.ToString(), NewHeader?.Style.ToString())); + + if (Explode) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Explode", OldHeader?.Explode.ToString(), NewHeader?.Explode.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs new file mode 100644 index 000000000..c9c7d1133 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedHeadersBO.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedHeadersBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Header; + + private readonly IDictionary _oldHeaders; + private readonly IDictionary _newHeaders; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedHeadersBO(IDictionary oldHeaders, IDictionary newHeaders, DiffContextBO context) + { + _oldHeaders = oldHeaders; + _newHeaders = newHeaders; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs new file mode 100644 index 000000000..376ac17e7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfoBO.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedInfoBO + { + public ChangedElementTypeEnum ElementType { get; } + public TypeEnum ChangeType { get; } + public string FieldName { get; } + public string OldValue { get; } + public string NewValue { get; } + + public ChangedInfoBO(ChangedElementTypeEnum elementType, TypeEnum changeType, string fieldName, string oldValue, string newValue) + { + ElementType = elementType; + ChangeType = changeType; + FieldName = fieldName; + OldValue = oldValue; + NewValue = newValue; + } + + public static ChangedInfoBO ForAdded(ChangedElementTypeEnum elementType, string fieldName) + { + return new ChangedInfoBO(elementType, TypeEnum.Added, fieldName, null, null); + } + + public static ChangedInfoBO ForRemoved(ChangedElementTypeEnum elementType, string fieldName) + { + return new ChangedInfoBO(elementType, TypeEnum.Removed, fieldName, null, null); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs new file mode 100644 index 000000000..8ae8761ce --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedInfosBO.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedInfosBO + { + public List Path { get; set; } + public DiffResultBO ChangeType { get; set; } + public List Changes { get; set; } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs new file mode 100644 index 000000000..6ac77a5f4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedListBO.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ChangedListBO : ChangedBO + { + public readonly DiffContextBO Context; + public readonly IList OldValue; + public readonly IList NewValue; + + public List Increased { get; set; } + public List Missing { get; set; } + public List Shared { get; set; } + + protected ChangedListBO(IList oldValue, IList newValue, DiffContextBO context) + { + OldValue = oldValue; + NewValue = newValue; + Context = context; + Shared = new List(); + Increased = new List(); + Missing = new List(); + } + + public override DiffResultBO IsChanged() + { + if (Missing.IsNullOrEmpty() && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + return IsItemsChanged(); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + + foreach (var listElement in Increased) + { + returnList.Add(ChangedInfoBO.ForAdded(elementType, listElement.ToString())); + } + + foreach (var listElement in Missing) + { + returnList.Add(ChangedInfoBO.ForRemoved(elementType, listElement.ToString())); + } + return returnList; + } + + public abstract DiffResultBO IsItemsChanged(); + + //public class SimpleChangedList : ChangedListBO + //{ + // protected SimpleChangedList(List oldValue, List newValue) : base(oldValue, newValue, null) + // { + // } + + // public override DiffResultBO IsItemsChanged() + // { + // return new DiffResultBO(DiffResultEnum.Unknown); + // } + //} + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs new file mode 100644 index 000000000..83a885ed8 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMaxLengthBO.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMaxLengthBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.MaxLength; + + private readonly int? _oldValue; + private readonly int? _newValue; + private readonly DiffContextBO _context; + + public ChangedMaxLengthBO(int? oldValue, int? newValue, DiffContextBO context) + { + _oldValue = oldValue; + _newValue = newValue; + _context = context; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest && (_newValue == null || _oldValue != null && _oldValue <= _newValue) + || _context.IsResponse && (_oldValue == null || _newValue != null && _newValue <= _oldValue)) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue?.ToString(), _newValue?.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs new file mode 100644 index 000000000..4341fb3cf --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMediaTypeBO.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMediaTypeBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.MediaType; + + private readonly OpenApiSchema _oldSchema; + private readonly OpenApiSchema _newSchema; + private readonly DiffContextBO _context; + + public ChangedSchemaBO Schema { get; set; } + + public ChangedMediaTypeBO(OpenApiSchema oldSchema, OpenApiSchema newSchema, DiffContextBO context) + { + _oldSchema = oldSchema; + _newSchema = newSchema; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Schema), + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs new file mode 100644 index 000000000..c5bde4f3b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedMetadataBO.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedMetadataBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Metadata; + + public string Left { get; } + public string Right { get; } + + public ChangedMetadataBO(string left, string right) + { + Left = left; + Right = right; + } + + public override DiffResultBO IsChanged() + { + return Left == Right ? new DiffResultBO(DiffResultEnum.NoChanges) : new DiffResultBO(DiffResultEnum.Metadata); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (Left != Right) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Value", Left, Right)); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs new file mode 100644 index 000000000..73f85bbf3 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowBO.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOAuthFlowBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.AuthFlow; + + public OpenApiOAuthFlow OldOAuthFlow { get; } + public OpenApiOAuthFlow NewOAuthFlow { get; } + + public bool ChangedAuthorizationUrl { get; set; } + public bool ChangedTokenUrl { get; set; } + public bool ChangedRefreshUrl { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOAuthFlowBO(OpenApiOAuthFlow oldOAuthFlow, OpenApiOAuthFlow newOAuthFlow) + { + OldOAuthFlow = oldOAuthFlow; + NewOAuthFlow = newOAuthFlow; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (ChangedAuthorizationUrl || ChangedTokenUrl || ChangedRefreshUrl) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (ChangedAuthorizationUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "AuthorizationUrl", OldOAuthFlow?.AuthorizationUrl.ToString(), NewOAuthFlow?.AuthorizationUrl.ToString())); + + if (ChangedTokenUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "TokenUrl", OldOAuthFlow?.TokenUrl.ToString(), NewOAuthFlow?.TokenUrl.ToString())); + + if (ChangedRefreshUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "RefreshUrl", OldOAuthFlow?.RefreshUrl.ToString(), NewOAuthFlow?.RefreshUrl.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs new file mode 100644 index 000000000..11365b04e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOAuthFlowsBO.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOAuthFlowsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.AuthFlow; + + private readonly OpenApiOAuthFlows _oldOAuthFlows; + private readonly OpenApiOAuthFlows _newOAuthFlows; + + public ChangedOAuthFlowBO ImplicitOAuthFlow { get; set; } + public ChangedOAuthFlowBO PasswordOAuthFlow { get; set; } + public ChangedOAuthFlowBO ClientCredentialOAuthFlow { get; set; } + public ChangedOAuthFlowBO AuthorizationCodeOAuthFlow { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOAuthFlowsBO(OpenApiOAuthFlows oldOAuthFlows, OpenApiOAuthFlows newOAuthFlows) + { + _oldOAuthFlows = oldOAuthFlows; + _newOAuthFlows = newOAuthFlows; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Implicit", ImplicitOAuthFlow), + ("Password", PasswordOAuthFlow), + ("ClientCredential", ClientCredentialOAuthFlow), + ("AuthorizationCode", AuthorizationCodeOAuthFlow), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs new file mode 100644 index 000000000..24d2ffedf --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOneOfSchemaBO.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOneOfSchemaBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.OneOf; + + private readonly Dictionary _oldMapping; + private readonly Dictionary _newMapping; + public DiffContextBO Context { get; } + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedOneOfSchemaBO( Dictionary oldMapping, Dictionary newMapping, DiffContextBO context) + { + _oldMapping = oldMapping; + _newMapping = newMapping; + Context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Context.IsRequest && Missing.IsNullOrEmpty() || Context.IsResponse && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs new file mode 100644 index 000000000..a81ff5e48 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOpenApiBO.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOpenApiBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.OpenApi; + public string OldSpecIdentifier { get; set; } + public string NewSpecIdentifier { get; set; } + public OpenApiDocument OldSpecOpenApi { get; set; } + public OpenApiDocument NewSpecOpenApi { get; set; } + public List NewEndpoints { get; set; } + public List MissingEndpoints { get; set; } + public List ChangedOperations { get; set; } + public ChangedExtensionsBO ChangedExtensions { get; set; } + + public ChangedOpenApiBO(string oldSpecIdentifier, string newSpecIdentifier) + { + NewEndpoints = new List(); + MissingEndpoints = new List(); + ChangedOperations = new List(); + OldSpecIdentifier = oldSpecIdentifier; + NewSpecIdentifier = newSpecIdentifier; + } + public List GetDeprecatedEndpoints() + { + return ChangedOperations + .Where(x => x.IsDeprecated) + .Select(x => x.ConvertToEndpoint()) + .ToList(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> ( + ChangedOperations.Select(x => (x.PathUrl, (ChangedBO)x)) + ) + { + (null, ChangedExtensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (NewEndpoints.IsNullOrEmpty() && MissingEndpoints.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (MissingEndpoints.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(NewEndpoints, MissingEndpoints, x => x.PathUrl); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs new file mode 100644 index 000000000..a81980b1c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedOperationBO.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedOperationBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Operation; + + public OpenApiOperation OldOperation { get; } + public OpenApiOperation NewOperation { get; } + + public OperationType HttpMethod { get; } + public string PathUrl { get; } + public ChangedMetadataBO Summary { get; set; } + public ChangedMetadataBO Description { get; set; } + public bool IsDeprecated { get; set; } + public ChangedParametersBO Parameters { get; set; } + public ChangedRequestBodyBO RequestBody { get; set; } + public ChangedAPIResponseBO APIResponses { get; set; } + public ChangedSecurityRequirementsBO SecurityRequirements { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedOperationBO(string pathUrl, OperationType httpMethod, OpenApiOperation oldOperation, OpenApiOperation newOperation) + { + PathUrl = pathUrl; + HttpMethod = httpMethod; + OldOperation = oldOperation; + NewOperation = newOperation; + } + + public EndpointBO ConvertToEndpoint() + { + var endpoint = new EndpointBO + { + PathUrl = PathUrl, + Method = HttpMethod, + Summary = NewOperation.Summary, + Operation = NewOperation + }; + return endpoint; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Summary", Summary), + ("Description", Description), + ("Parameters", Parameters), + ("RequestBody", RequestBody), + ("Responses", APIResponses), + ("SecurityRequirements", SecurityRequirements), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (IsDeprecated) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldOperation?.Deprecated.ToString(), NewOperation?.Deprecated.ToString())); + + return returnList; + } + + public DiffResultBO ResultApiResponses() + { + return Result(APIResponses); + } + + public DiffResultBO ResultRequestBody() + { + return RequestBody == null ? new DiffResultBO(DiffResultEnum.NoChanges) : RequestBody.IsChanged(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs new file mode 100644 index 000000000..dccc6fe2d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParameterBO.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedParameterBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Parameter; + + private readonly DiffContextBO _context; + public ParameterLocation? In { get; set; } + public string Name { get; set; } + public OpenApiParameter OldParameter { get; } + public OpenApiParameter NewParameter { get; } + public bool IsChangeRequired { get; set; } + public bool IsDeprecated { get; set; } + public bool ChangeStyle { get; set; } + public bool ChangeExplode { get; set; } + public bool ChangeAllowEmptyValue { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedSchemaBO Schema { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedParameterBO(string name, ParameterLocation? @in, OpenApiParameter oldParameter, OpenApiParameter newParameter, DiffContextBO context) + { + _context = context; + Name = name; + In = @in; + OldParameter = oldParameter; + NewParameter = newParameter; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, Schema), + (null, Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!IsChangeRequired + && !IsDeprecated + && !ChangeAllowEmptyValue + && !ChangeStyle + && !ChangeExplode) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if ((!IsChangeRequired || OldParameter.Required) + && (!ChangeAllowEmptyValue || NewParameter.AllowEmptyValue) + && !ChangeStyle + && !ChangeExplode) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangeRequired) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", OldParameter?.Required.ToString(), NewParameter?.Required.ToString())); + + if (IsDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldParameter?.Deprecated.ToString(), NewParameter?.Deprecated.ToString())); + + if (ChangeStyle) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Style", OldParameter?.Style.ToString(), NewParameter?.Style.ToString())); + + if (ChangeExplode) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Explode", OldParameter?.Explode.ToString(), NewParameter?.Explode.ToString())); + + if (ChangeAllowEmptyValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, "AllowEmptyValue", OldParameter?.AllowEmptyValue.ToString(), NewParameter?.AllowEmptyValue.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs new file mode 100644 index 000000000..cca278d73 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedParametersBO.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedParametersBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Parameter; + + private readonly List _oldParameterList; + private readonly List _newParameterList; + private readonly DiffContextBO _context; + public List Increased { get; set; } + public List Missing { get; set; } + public List Changed { get; set; } + + public ChangedParametersBO(List oldParameterList, List newParameterList, DiffContextBO context) + { + _oldParameterList = oldParameterList; + _newParameterList = newParameterList; + _context = context; + Increased = new List(); + Missing = new List(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> ( + Changed.Select(x => (x.Name, (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + if (Increased.Any(x => x.Required) && Missing.IsNullOrEmpty()) + return new DiffResultBO(DiffResultEnum.Compatible); + + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased, Missing, x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs new file mode 100644 index 000000000..c154ebe52 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathBO.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedPathBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Path; + + private readonly string _pathUrl; + private readonly OpenApiPathItem _oldPath; + private readonly OpenApiPathItem _newPath; + private readonly DiffContextBO _context; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public List Changed { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedPathBO(string pathUrl, OpenApiPathItem oldPath, OpenApiPathItem newPath, DiffContextBO context) + { + _pathUrl = pathUrl; + _oldPath = oldPath; + _newPath = newPath; + _context = context; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.PathUrl, (ChangedBO)x)) + ) + { + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x.ToString()); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs new file mode 100644 index 000000000..a3af1f0ec --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedPathsBO.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedPathsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Path; + + private readonly Dictionary _oldPathMap; + private readonly Dictionary _newPathMap; + + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public Dictionary Changed { get; set; } + + public ChangedPathsBO(Dictionary oldPathMap, Dictionary newPathMap) + { + _oldPathMap = oldPathMap; + _newPathMap = newPathMap; + Increased = new Dictionary(); + Missing = new Dictionary(); + Changed = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased.IsNullOrEmpty() && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.ToList(), Missing.ToList(), x => x.Key); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs new file mode 100644 index 000000000..b2a726d22 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedReadOnlyBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedReadOnlyBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.ReadOnly; + + private readonly DiffContextBO _context; + private readonly bool _oldValue; + private readonly bool _newValue; + + public ChangedReadOnlyBO(bool? oldValue, bool? newValue, DiffContextBO context) + { + _context = context; + _oldValue = oldValue ?? false; + _newValue = newValue ?? false; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsResponse) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + if (_context.IsRequest) + { + if (_newValue) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + return _context.IsRequired ? new DiffResultBO(DiffResultEnum.Incompatible) : new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Unknown); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue.ToString(), _newValue.ToString())); + + return returnList; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs new file mode 100644 index 000000000..432b9f8e5 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequestBodyBO.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedRequestBodyBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.RequestBody; + + private readonly OpenApiRequestBody _oldRequestBody; + private readonly OpenApiRequestBody _newRequestBody; + private readonly DiffContextBO _context; + + public bool ChangeRequired { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedRequestBodyBO(OpenApiRequestBody oldRequestBody, OpenApiRequestBody newRequestBody, DiffContextBO context) + { + _oldRequestBody = oldRequestBody; + _newRequestBody = newRequestBody; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + ("Description", Description), + ("Content", Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!ChangeRequired) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (_oldRequestBody?.Required != _newRequestBody?.Required) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Required", _oldRequestBody?.Required.ToString(), _newRequestBody?.Required.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs new file mode 100644 index 000000000..e044a301d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedRequiredBO.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedRequiredBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Required; + + public ChangedRequiredBO(IList oldValue, IList newValue, DiffContextBO context) : base(oldValue, newValue, context) + { + } + + public override DiffResultBO IsItemsChanged() + { + if (Context.IsRequest && Increased.IsNullOrEmpty() + || Context.IsResponse && Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs new file mode 100644 index 000000000..7c366fe27 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedResponseBO.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedResponseBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Response; + + private readonly DiffContextBO _context; + public OpenApiResponse OldApiResponse { get; } + public OpenApiResponse NewApiResponse { get; } + public ChangedMetadataBO Description { get; set; } + public ChangedHeadersBO Headers { get; set; } + public ChangedContentBO Content { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedResponseBO(OpenApiResponse oldApiResponse, OpenApiResponse newApiResponse, DiffContextBO context) + { + OldApiResponse = oldApiResponse; + NewApiResponse = newApiResponse; + _context = context; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, Headers), + (null, Content), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + + protected override List GetCoreChanges() + { + return new List(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs new file mode 100644 index 000000000..87b25fb32 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSchemaBO.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSchemaBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.Schema; + + public DiffContextBO Context { get; set; } + public OpenApiSchema OldSchema { get; set; } + public OpenApiSchema NewSchema { get; set; } + public string Type { get; set; } + public Dictionary ChangedProperties { get; set; } + public Dictionary IncreasedProperties { get; set; } + public Dictionary MissingProperties { get; set; } + public bool IsChangeDeprecated { get; set; } + public ChangedMetadataBO Description { get; set; } + public bool IsChangeTitle { get; set; } + public ChangedRequiredBO Required { get; set; } + public bool IsChangeDefault { get; set; } + public ChangedEnumBO Enumeration { get; set; } + public bool IsChangeFormat { get; set; } + public ChangedReadOnlyBO ReadOnly { get; set; } + public ChangedWriteOnlyBO WriteOnly { get; set; } + public bool IsChangedType { get; set; } + public ChangedMaxLengthBO MaxLength { get; set; } + public bool DiscriminatorPropertyChanged { get; set; } + public ChangedSchemaBO Items { get; set; } + public ChangedOneOfSchemaBO OneOfSchema { get; set; } + public ChangedSchemaBO AddProp { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedSchemaBO() + { + IncreasedProperties = new Dictionary(); + MissingProperties = new Dictionary(); + ChangedProperties = new Dictionary(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + ChangedProperties.Select(x => (x.Key, (ChangedBO)x.Value)) + ) + { + ("Description", Description), + ("ReadOnly", ReadOnly), + ("WriteOnly", WriteOnly), + ("Items", Items), + ("OneOfSchema", OneOfSchema), + ("AddProp", AddProp), + ("Enumeration", Enumeration), + ("Required", Required), + ("MaxLength", MaxLength), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if ( + !IsChangedType + && (OldSchema == null && NewSchema == null || OldSchema != null && NewSchema != null) + && !IsChangeFormat + && IncreasedProperties.Count == 0 + && MissingProperties.Count == 0 + && ChangedProperties.Values.Count == 0 + && !IsChangeDeprecated + && ! DiscriminatorPropertyChanged + ) + return new DiffResultBO(DiffResultEnum.NoChanges); + + var compatibleForRequest = OldSchema != null || NewSchema == null; + var compatibleForResponse = + MissingProperties.IsNullOrEmpty() && (OldSchema == null || NewSchema != null); + + if ((Context.IsRequest && compatibleForRequest + || Context.IsResponse && compatibleForResponse) + && !IsChangedType + && !DiscriminatorPropertyChanged) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = GetCoreChangeInfosOfComposed(IncreasedProperties.Keys.ToList(), MissingProperties.Keys.ToList(), x => x); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangedType) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Type", OldSchema?.Type, NewSchema?.Type)); + + if (IsChangeDefault) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Default", OldSchema?.Default.ToString(), NewSchema?.Default.ToString())); + + if (IsChangeDeprecated) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Deprecation", OldSchema?.Deprecated.ToString(), NewSchema?.Deprecated.ToString())); + + if (IsChangeFormat) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Format", OldSchema?.Format, NewSchema?.Format)); + + if (IsChangeTitle) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Title", OldSchema?.Title, NewSchema?.Title)); + + if (DiscriminatorPropertyChanged) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Discriminator Property", OldSchema?.Discriminator?.PropertyName, NewSchema?.Discriminator?.PropertyName)); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs new file mode 100644 index 000000000..b7181d5ea --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementBO.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecurityRequirementBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityRequirement; + + private readonly OpenApiSecurityRequirement _oldSecurityRequirement; + private readonly OpenApiSecurityRequirement _newSecurityRequirement; + + public OpenApiSecurityRequirement Missing { get; set; } + public OpenApiSecurityRequirement Increased { get; set; } + public List Changed { get; set; } + + public ChangedSecurityRequirementBO(OpenApiSecurityRequirement newSecurityRequirement, OpenApiSecurityRequirement oldSecurityRequirement) + { + _newSecurityRequirement = newSecurityRequirement; + _oldSecurityRequirement = oldSecurityRequirement; + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (x.NewSecurityScheme.Name ?? x.OldSecurityScheme.Name, (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Increased == null && Missing == null) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Increased == null) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.Keys.ToList(), Missing.Keys.ToList(), x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs new file mode 100644 index 000000000..35509be31 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecurityRequirementsBO.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecurityRequirementsBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityRequirement; + + private readonly IList _oldSecurityRequirements; + private readonly IList _newSecurityRequirements; + + public List Missing { get; set; } + public List Increased { get; set; } + public List Changed { get; set; } + + public ChangedSecurityRequirementsBO(IList oldSecurityRequirements, IList newSecurityRequirements) + { + _oldSecurityRequirements = oldSecurityRequirements; + _newSecurityRequirements = newSecurityRequirements; + Missing = new List(); + Increased = new List(); + Changed = new List(); + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)>( + Changed.Select(x => (GetElementType().GetDisplayName(), (ChangedBO)x)) + ) + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (Missing.IsNullOrEmpty() && Increased.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (Missing.IsNullOrEmpty()) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() => + GetCoreChangeInfosOfComposed(Increased.SelectMany(x => x.Keys).ToList(), Missing.SelectMany(x => x.Keys).ToList(), x => x.Name); + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs new file mode 100644 index 000000000..103193846 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeBO.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecuritySchemeBO : ComposedChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecurityScheme; + + public OpenApiSecurityScheme OldSecurityScheme { get; } + public OpenApiSecurityScheme NewSecurityScheme { get; } + + public bool IsChangedType { get; set; } + public bool IsChangedIn { get; set; } + public bool IsChangedScheme { get; set; } + public bool IsChangedBearerFormat { get; set; } + public bool IsChangedOpenIdConnectUrl { get; set; } + public ChangedSecuritySchemeScopesBO ChangedScopes { get; set; } + public ChangedMetadataBO Description { get; set; } + public ChangedOAuthFlowsBO OAuthFlows { get; set; } + public ChangedExtensionsBO Extensions { get; set; } + + public ChangedSecuritySchemeBO(OpenApiSecurityScheme oldSecurityScheme, OpenApiSecurityScheme newSecurityScheme) + { + OldSecurityScheme = oldSecurityScheme; + NewSecurityScheme = newSecurityScheme; + } + + public override List<(string Identifier, ChangedBO Change)> GetChangedElements() + { + return new List<(string Identifier, ChangedBO Change)> + { + (null, Description), + (null, OAuthFlows), + (null, Extensions) + } + .Where(x => x.Change != null).ToList(); + } + + public override DiffResultBO IsCoreChanged() + { + if (!IsChangedType + && !IsChangedIn + && !IsChangedScheme + && !IsChangedBearerFormat + && !IsChangedOpenIdConnectUrl + && (ChangedScopes == null || ChangedScopes.IsUnchanged())) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (!IsChangedType + && !IsChangedIn + && !IsChangedScheme + && !IsChangedBearerFormat + && !IsChangedOpenIdConnectUrl + && (ChangedScopes == null || ChangedScopes.Increased.IsNullOrEmpty())) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if (IsChangedBearerFormat) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Bearer Format", OldSecurityScheme?.BearerFormat, NewSecurityScheme?.BearerFormat)); + + if (IsChangedIn) + returnList.Add(new ChangedInfoBO(elementType, changeType, "In", OldSecurityScheme?.In.ToString(), NewSecurityScheme?.In.ToString())); + + if (IsChangedOpenIdConnectUrl) + returnList.Add(new ChangedInfoBO(elementType, changeType, "OpenIdConnect Url", OldSecurityScheme?.OpenIdConnectUrl.ToString(), NewSecurityScheme?.OpenIdConnectUrl.ToString())); + + if (IsChangedScheme) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Scheme", OldSecurityScheme?.Scheme, NewSecurityScheme?.Scheme)); + + if (IsChangedType) + returnList.Add(new ChangedInfoBO(elementType, changeType, "Type", OldSecurityScheme?.Type.ToString(), NewSecurityScheme?.Type.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs new file mode 100644 index 000000000..33ee349d4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedSecuritySchemeScopesBO.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedSecuritySchemeScopesBO : ChangedListBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.SecuritySchemeScope; + + public ChangedSecuritySchemeScopesBO(List oldValue, List newValue) : base(oldValue, newValue, null) + { + } + + public override DiffResultBO IsItemsChanged() + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs new file mode 100644 index 000000000..db0613fe9 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ChangedWriteOnlyBO.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class ChangedWriteOnlyBO : ChangedBO + { + protected override ChangedElementTypeEnum GetElementType() => ChangedElementTypeEnum.WriteOnly; + + private readonly DiffContextBO _context; + private readonly bool _oldValue; + private readonly bool _newValue; + + public ChangedWriteOnlyBO(bool? oldValue, bool? newValue, DiffContextBO context) + { + _context = context; + _oldValue = oldValue ?? false; + _newValue = newValue ?? false; + } + + public override DiffResultBO IsChanged() + { + if (_oldValue == _newValue) + { + return new DiffResultBO(DiffResultEnum.NoChanges); + } + if (_context.IsRequest) + { + return new DiffResultBO(DiffResultEnum.Compatible); + } + if (_context.IsResponse) + { + if (_newValue) + { + return new DiffResultBO(DiffResultEnum.Incompatible); + } + + return _context.IsRequired ? new DiffResultBO(DiffResultEnum.Incompatible) : new DiffResultBO(DiffResultEnum.Compatible); + } + return new DiffResultBO(DiffResultEnum.Unknown); + } + + protected override List GetCoreChanges() + { + var returnList = new List(); + var elementType = GetElementType(); + const TypeEnum changeType = TypeEnum.Changed; + + if(_oldValue != _newValue) + returnList.Add(new ChangedInfoBO(elementType, changeType, _context.GetDiffContextElementType(), _oldValue.ToString(), _newValue.ToString())); + + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs new file mode 100644 index 000000000..5ac556bb7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/ComposedChangedBO.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public abstract class ComposedChangedBO : ChangedBO + { + protected ComposedChangedBO() + { + } + + public abstract List<(string Identifier, ChangedBO Change)> GetChangedElements(); + + public override List GetAllChangeInfoFlat(string identifier, List parentPath = null) + { + var coreChangeInfo = GetCoreChangeInfo(identifier, parentPath); + var changedElements = GetChangedElements(); + var returnList = changedElements + .SelectMany(x => x.Change.GetAllChangeInfoFlat(x.Identifier, coreChangeInfo.Path)) + .Where(x => !x.ChangeType.IsUnchanged()) + .OrderBy(x => x.Path.Count) + .ToList(); + + returnList.Add(coreChangeInfo); + + return returnList; + } + + public override DiffResultBO IsChanged() + { + var elementsResultMax = GetChangedElements() + .Where(x => x.Change != null) + .Select(x => (int)x.Change.IsChanged().DiffResult) + .DefaultIfEmpty(0) + .Max(); + + var elementsResult = new DiffResultBO((DiffResultEnum)elementsResultMax); + + return IsCoreChanged().DiffResult > elementsResult.DiffResult ? IsCoreChanged() : elementsResult; + } + + protected List GetCoreChangeInfosOfComposed(List increased, List missing, Func identifierSelector) + { + var returnList = new List(); + var elementType = GetElementType(); + + foreach (var listElement in increased) + { + returnList.Add(ChangedInfoBO.ForAdded(elementType, identifierSelector(listElement))); + } + + foreach (var listElement in missing) + { + returnList.Add(ChangedInfoBO.ForRemoved(elementType, identifierSelector(listElement))); + } + return returnList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs new file mode 100644 index 000000000..b0888f391 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffContextBO.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class DiffContextBO + { + public string URL { get; set; } + public Dictionary Parameters { get; set; } + public OperationType Method { get; private set; } + public bool IsResponse { get; private set; } + public bool IsRequest { get; private set; } + + public bool IsRequired { get; private set; } + + public DiffContextBO() + { + Parameters = new Dictionary(); + IsResponse = false; + IsRequest = true; + } + + public string GetDiffContextElementType() => IsResponse ? "Response" : "Request"; + + + public DiffContextBO CopyWithMethod(OperationType method) + { + var result = Copy(); + result.Method = method; + return result; + } + + public DiffContextBO CopyWithRequired(bool required) + { + var result = Copy(); + result.IsRequired = required; + return result; + } + + public DiffContextBO CopyAsRequest() + { + var result = Copy(); + result.IsRequest = true; + result.IsResponse = false; + return result; + } + + public DiffContextBO CopyAsResponse() + { + var result = Copy(); + result.IsResponse = true; + result.IsRequest = false; + return result; + } + + private DiffContextBO Copy() + { + var context = new DiffContextBO + { + URL = URL, + Parameters = Parameters, + Method = Method, + IsResponse = IsResponse, + IsRequest = IsRequest, + IsRequired = IsRequired + }; + return context; + } + public override bool Equals(object o) + { + if (this == o) return true; + + if (o == null || GetType() != o.GetType()) return false; + + var that = (DiffContextBO)o; + + return IsResponse.Equals(that.IsResponse) + && IsRequest.Equals(that.IsRequest) + && URL.Equals(that.URL) + && Parameters.Equals(that.Parameters) + && Method.Equals(that.Method) + && IsRequired.Equals(that.IsRequired); + } + + public override int GetHashCode() + { + return HashCode.Combine(URL, Parameters, Method, IsResponse, IsRequest, IsRequired); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs new file mode 100644 index 000000000..c1b6b9b7c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/DiffResultBO.cs @@ -0,0 +1,39 @@ +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class DiffResultBO + { + public readonly DiffResultEnum DiffResult; + + public DiffResultBO(DiffResultEnum diffResult) + { + DiffResult = diffResult; + } + + public bool IsUnchanged() + { + return DiffResult == 0; + } + + public bool IsDifferent() + { + return DiffResult > 0; + } + + public bool IsIncompatible() + { + return (int)DiffResult > 2; + } + + public bool IsCompatible() + { + return (int)DiffResult <= 2; + } + + public bool IsMetaChanged() + { + return (int)DiffResult == 1; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs b/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs new file mode 100644 index 000000000..ba96adf0f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/BusinessObjects/EndpointBO.cs @@ -0,0 +1,18 @@ +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.BusinessObjects +{ + public class EndpointBO + { + public string PathUrl { get; set; } + public OperationType Method { get; set; } + public string Summary { get; set; } + public OpenApiPathItem Path { get; set; } + public OpenApiOperation Operation { get; set; } + + public override string ToString() + { + return Method + " " + PathUrl; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs new file mode 100644 index 000000000..c47104e78 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ApiResponseDiff.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ApiResponseDiff + { + private readonly OpenApiDiff _openApiDiff; + + public ApiResponseDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedAPIResponseBO Diff(OpenApiResponses left, OpenApiResponses right, DiffContextBO context) + { + var responseMapKeyDiff = MapKeyDiff.Diff(left, right); + var sharedResponseCodes = responseMapKeyDiff.SharedKey; + var responses = new Dictionary(); + foreach (var responseCode in sharedResponseCodes) + { + var diff = _openApiDiff + .ResponseDiff + .Diff(left[responseCode], right[responseCode], context); + + if(diff!= null) + responses.Add(responseCode, diff); + } + + var changedApiResponse = + new ChangedAPIResponseBO(left, right, context) + { + Increased = responseMapKeyDiff.Increased, + Missing = responseMapKeyDiff.Missing, + Changed = responses, + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedApiResponse); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs b/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs new file mode 100644 index 000000000..5df74dd47 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/CacheKey.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class CacheKey : IEquatable + { + private readonly string left; + private readonly string right; + private readonly DiffContextBO context; + + public CacheKey(string left, string right, DiffContextBO context) + { + this.left = left; + this.right = right; + this.context = context; + } + + public override bool Equals(object obj) + { + if (this == obj) return true; + + if (obj == null || GetType() != obj.GetType()) return false; + + var cacheKey = (CacheKey)obj; + + return Equals(cacheKey); + } + + public bool Equals(CacheKey other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return left == other.left && right == other.right && Equals(context, other.context); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (left != null ? left.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (right != null ? right.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (context != null ? context.GetHashCode() : 0); + return hashCode; + } + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs new file mode 100644 index 000000000..b2f698f53 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ContentDiff.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ContentDiff : IEquatable> + { + private readonly OpenApiDiff _openApiDiff; + + public ContentDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public bool Equals(IDictionary other) + { + return false; + } + + public ChangedContentBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + var leftDict = (Dictionary)left; + var rightDict = (Dictionary)right; + + + var mediaTypeDiff = MapKeyDiff.Diff(leftDict, rightDict); + var sharedMediaTypes = mediaTypeDiff.SharedKey; + var changedMediaTypes = new Dictionary(); + foreach (var sharedMediaType in sharedMediaTypes) + { + var oldMediaType = left[sharedMediaType]; + var newMediaType = right[sharedMediaType]; + var changedMediaType = + new ChangedMediaTypeBO(oldMediaType?.Schema, newMediaType?.Schema, context) + { + Schema = _openApiDiff + .SchemaDiff + .Diff( + new HashSet(), + oldMediaType?.Schema, + newMediaType?.Schema, + context.CopyWithRequired(true)) + }; + if (!ChangedUtils.IsUnchanged(changedMediaType)) + { + changedMediaTypes.Add(sharedMediaType, changedMediaType); + } + } + + return ChangedUtils.IsChanged(new ChangedContentBO(leftDict, rightDict, context) + { + Increased = mediaTypeDiff.Increased, + Missing = mediaTypeDiff.Missing, + Changed = changedMediaTypes + }); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs new file mode 100644 index 000000000..e1b65d5ea --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ExtensionDiff.cs @@ -0,0 +1,20 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public abstract class ExtensionDiff : IExtensionDiff + { + public abstract ExtensionDiff SetOpenApiDiff(OpenApiDiff openApiDiff); + + public abstract string GetName(); + + public abstract ChangedBO Diff(ChangeBO extension, DiffContextBO context) + where T : class; + + public virtual bool IsParentApplicable(TypeEnum type, object objectElement, object extension, DiffContextBO context) + { + return true; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs new file mode 100644 index 000000000..8f434e117 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ExtensionsDiff.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ExtensionsDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly IEnumerable _extensions; + + public ExtensionsDiff(OpenApiDiff openApiDiff, IEnumerable extensions) + { + _openApiDiff = openApiDiff; + _extensions = extensions; + } + + public bool IsParentApplicable(TypeEnum type, object parent, IDictionary extensions, DiffContextBO context) + { + if (extensions.IsNullOrEmpty()) + return true; + + return extensions.Select(x => ExecuteExtension(x.Key, y => y + .IsParentApplicable(type, parent, x.Value, context))) + .All(x => x); + } + + public IExtensionDiff GetExtensionDiff(string name) + { + if (_extensions.IsNullOrEmpty()) + return null; + + return _extensions.FirstOrDefault(x => $"x-{x.GetName()}" == name); + } + + public T ExecuteExtension(string name, Func predicate) + { + if (_extensions.IsNullOrEmpty()) + return default; + + return predicate(GetExtensionDiff(name).SetOpenApiDiff(_openApiDiff)); + } + + public ChangedExtensionsBO Diff(IDictionary left, IDictionary right) + { + return Diff(left, right, null); + } + + public ChangedExtensionsBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + left = ((Dictionary)left).CopyDictionary(); + right = ((Dictionary)right).CopyDictionary(); + var changedExtensions = new ChangedExtensionsBO((Dictionary)left, ((Dictionary)right).CopyDictionary(), context); + foreach (var (key, value) in left) + { + if (right.ContainsKey(key)) + { + var rightValue = right[key]; + right.Remove(key); + var changed = ExecuteExtensionDiff(key, ChangeBO.Changed(value, rightValue), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Changed.Add(key, changed); + } + else + { + var changed = ExecuteExtensionDiff(key, ChangeBO.Removed(value), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Missing.Add(key, changed); + } + } + + foreach (var (key, value) in right) + { + var changed = ExecuteExtensionDiff(key, ChangeBO.Added(value), context); + if (changed?.IsDifferent() ?? false) + changedExtensions.Increased.Add(key, changed); + } + + return ChangedUtils.IsChanged(changedExtensions); + } + + private ChangedBO ExecuteExtensionDiff(string name, ChangeBO change, DiffContextBO context) + where T : class + { + return ExecuteExtension(name, x => x.Diff(change, context)); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs new file mode 100644 index 000000000..63f0afcb2 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/HeaderDiff.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class HeaderDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Headers); + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public HeaderDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedHeaderBO Diff(OpenApiHeader left, OpenApiHeader right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedHeaderBO ComputeDiff(HashSet refSet, OpenApiHeader left, OpenApiHeader right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedHeader = + new ChangedHeaderBO(left, right, context) + { + Required = GetBooleanDiff(left.Required, right.Required), + Deprecated = !left.Deprecated && right.Deprecated, + Style = left.Style != right.Style, + Explode = GetBooleanDiff(left.Explode, right.Explode), + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Schema = _openApiDiff + .SchemaDiff + .Diff(new HashSet(), left.Schema, right.Schema, context.CopyWithRequired(true)), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedHeader); + } + + private static bool GetBooleanDiff(bool? left, bool? right) + { + var leftRequired = left ?? false; + var rightRequired = right ?? false; + return leftRequired != rightRequired; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs new file mode 100644 index 000000000..58ce9113c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/HeadersDiff.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class HeadersDiff + { + private readonly OpenApiDiff _openApiDiff; + + public HeadersDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedHeadersBO Diff(IDictionary left, IDictionary right, DiffContextBO context) + { + var headerMapDiff = MapKeyDiff.Diff(left, right); + var sharedHeaderKeys = headerMapDiff.SharedKey; + + var changed = new Dictionary(); + foreach (var headerKey in sharedHeaderKeys) + { + var oldHeader = left[headerKey]; + var newHeader = right[headerKey]; + var changedHeaders = _openApiDiff + .HeaderDiff + .Diff(oldHeader, newHeader, context); + if (changedHeaders != null) + changed.Add(headerKey, changedHeaders); + } + + return ChangedUtils.IsChanged( + new ChangedHeadersBO(left, right, context) + { + Increased = headerMapDiff.Increased, + Missing = headerMapDiff.Missing, + Changed = changed + }); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs new file mode 100644 index 000000000..66ace7e55 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/IExtensionDiff.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public interface IExtensionDiff + { + ExtensionDiff SetOpenApiDiff(OpenApiDiff openApiDiff); + + string GetName(); + + ChangedBO Diff(ChangeBO extension, DiffContextBO context) + where T : class; + + bool IsParentApplicable(TypeEnum type, object objectElement, object extension, DiffContextBO context); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs new file mode 100644 index 000000000..faec10f06 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ListDiff.cs @@ -0,0 +1,42 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public static class ListDiff + { + public static T1 Diff(T1 instance) + where T1 : ChangedListBO + { + if (instance.OldValue.IsNullOrEmpty() && instance.NewValue.IsNullOrEmpty()) + { + return instance; + } + if (instance.OldValue.IsNullOrEmpty()) + { + instance.Increased = instance.NewValue.ToList(); + return instance; + } + if (instance.NewValue.IsNullOrEmpty()) + { + instance.Missing = instance.OldValue.ToList(); + return instance; + } + instance.Increased.AddRange(instance.NewValue); + foreach (var leftItem in instance.OldValue) + { + if (instance.NewValue.Contains(leftItem)) + { + instance.Increased.Remove(leftItem); + instance.Shared.Add(leftItem); + } + else + { + instance.Missing.Add(leftItem); + } + } + return instance; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs new file mode 100644 index 000000000..5d8238122 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/MapKeyDiff.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class MapKeyDiff + { + public Dictionary Increased { get; set; } + public Dictionary Missing { get; set; } + public List SharedKey { get; set; } + + private MapKeyDiff() + { + SharedKey = new List(); + Increased = new Dictionary(); + Missing = new Dictionary(); + } + + public static MapKeyDiff Diff(IDictionary mapLeft, IDictionary mapRight) + { + var instance = new MapKeyDiff(); + if (null == mapLeft && null == mapRight) return instance; + if (null == mapLeft) + { + instance.Increased = (Dictionary)mapRight; + return instance; + } + if (null == mapRight) + { + instance.Missing = (Dictionary)mapLeft; + return instance; + } + instance.Increased = new Dictionary(mapRight); + instance.Missing = new Dictionary(); + foreach (var entry in mapLeft) + { + var leftKey = entry.Key; + var leftValue = entry.Value; + if (mapRight.ContainsKey(leftKey)) + { + instance.Increased.Remove(leftKey); + instance.SharedKey.Add(leftKey); + } + else + { + instance.Missing.Add(leftKey, leftValue); + } + } + return instance; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs new file mode 100644 index 000000000..f3bbb3929 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/MetadataDiff.cs @@ -0,0 +1,25 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class MetadataDiff + { + private OpenApiComponents _leftComponents; + private OpenApiComponents _rightComponents; + private OpenApiDiff _openApiDiff; + + public MetadataDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedMetadataBO Diff(string left, string right, DiffContextBO context) + { + return ChangedUtils.IsChanged(new ChangedMetadataBO(left, right)); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs new file mode 100644 index 000000000..7e6177206 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowDiff.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OAuthFlowDiff + { + private readonly OpenApiDiff _openApiDiff; + public OAuthFlowDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOAuthFlowBO Diff(OpenApiOAuthFlow left, OpenApiOAuthFlow right) + { + var changedOAuthFlow = new ChangedOAuthFlowBO(left, right); + if (left != null && right != null) + { + changedOAuthFlow.ChangedAuthorizationUrl = left.AuthorizationUrl != right.AuthorizationUrl; + changedOAuthFlow.ChangedTokenUrl = left.TokenUrl != right.TokenUrl; + changedOAuthFlow.ChangedRefreshUrl = left.RefreshUrl != right.RefreshUrl; + } + + changedOAuthFlow.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left?.Extensions, right?.Extensions); + + return ChangedUtils.IsChanged(changedOAuthFlow); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs new file mode 100644 index 000000000..ac895bcfa --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OAuthFlowsDiff.cs @@ -0,0 +1,41 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OAuthFlowsDiff + { + private readonly OpenApiDiff _openApiDiff; + public OAuthFlowsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOAuthFlowsBO Diff(OpenApiOAuthFlows left, OpenApiOAuthFlows right) + { + var changedOAuthFlows = new ChangedOAuthFlowsBO(left, right); + if (left != null && right != null) + { + changedOAuthFlows.ImplicitOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.Implicit, right.Implicit); + changedOAuthFlows.PasswordOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.Password, right.Password); + changedOAuthFlows.ClientCredentialOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.ClientCredentials, right.ClientCredentials); + changedOAuthFlows.AuthorizationCodeOAuthFlow = _openApiDiff + .OAuthFlowDiff + .Diff(left.AuthorizationCode, right.AuthorizationCode); + } + + changedOAuthFlows.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left?.Extensions, right?.Extensions); + + return ChangedUtils.IsChanged(changedOAuthFlows); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs new file mode 100644 index 000000000..38bd3378a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OpenApiDiff.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OpenApiDiff + { + private readonly ILogger _logger; + + public string OldIdentifier { get; } + public string NewIdentifier { get; } + + public PathsDiff PathsDiff { get; set; } + public PathDiff PathDiff { get; set; } + public SchemaDiff SchemaDiff { get; set; } + public ContentDiff ContentDiff { get; set; } + public ParametersDiff ParametersDiff { get; set; } + public ParameterDiff ParameterDiff { get; set; } + public RequestBodyDiff RequestBodyDiff { get; set; } + public ResponseDiff ResponseDiff { get; set; } + public HeadersDiff HeadersDiff { get; set; } + public HeaderDiff HeaderDiff { get; set; } + public ApiResponseDiff APIResponseDiff { get; set; } + public OperationDiff OperationDiff { get; set; } + public SecurityRequirementsDiff SecurityRequirementsDiff { get; set; } + public SecurityRequirementDiff SecurityRequirementDiff { get; set; } + public SecuritySchemeDiff SecuritySchemeDiff { get; set; } + public OAuthFlowsDiff OAuthFlowsDiff { get; set; } + public OAuthFlowDiff OAuthFlowDiff { get; set; } + public ExtensionsDiff ExtensionsDiff { get; set; } + public MetadataDiff MetadataDiff { get; set; } + public OpenApiDocument OldSpecOpenApi { get; set; } + public OpenApiDocument NewSpecOpenApi { get; set; } + public List NewEndpoints { get; set; } + public List MissingEndpoints { get; set; } + public List ChangedOperations { get; set; } + public ChangedExtensionsBO ChangedExtensions { get; set; } + + public OpenApiDiff(OpenApiDocument oldSpecOpenApi, string oldSpecIdentifier, OpenApiDocument newSpecOpenApi, string newSpecIdentifier, IEnumerable extensions, ILogger logger) + { + _logger = logger; + OldSpecOpenApi = oldSpecOpenApi; + NewSpecOpenApi = newSpecOpenApi; + OldIdentifier = oldSpecIdentifier; + NewIdentifier = newSpecIdentifier; + + if (null == oldSpecOpenApi || null == newSpecOpenApi) + throw new Exception("one of the old or new object is null"); + + InitializeFields(extensions); + } + + public static ChangedOpenApiBO Compare(OpenApiDocument oldSpecOpenApi, string oldSpecIdentifier, OpenApiDocument newSpecOpenApi, string newSpecIdentifier, IEnumerable extensions, ILogger logger) + { + return new OpenApiDiff(oldSpecOpenApi, oldSpecIdentifier, newSpecOpenApi, newSpecIdentifier, extensions, logger).Compare(); + } + + private void InitializeFields(IEnumerable extensions) + { + PathsDiff = new PathsDiff(this); + PathDiff = new PathDiff(this); + SchemaDiff = new SchemaDiff(this); + ContentDiff = new ContentDiff(this); + ParametersDiff = new ParametersDiff(this); + ParameterDiff = new ParameterDiff(this); + RequestBodyDiff = new RequestBodyDiff(this); + ResponseDiff = new ResponseDiff(this); + HeadersDiff = new HeadersDiff(this); + HeaderDiff = new HeaderDiff(this); + APIResponseDiff = new ApiResponseDiff(this); + OperationDiff = new OperationDiff(this); + SecurityRequirementsDiff = new SecurityRequirementsDiff(this); + SecurityRequirementDiff = new SecurityRequirementDiff(this); + SecuritySchemeDiff = new SecuritySchemeDiff(this); + OAuthFlowsDiff = new OAuthFlowsDiff(this); + OAuthFlowDiff = new OAuthFlowDiff(this); + ExtensionsDiff = new ExtensionsDiff(this, extensions); + MetadataDiff = new MetadataDiff(this); + } + + private ChangedOpenApiBO Compare() + { + PreProcess(OldSpecOpenApi); + PreProcess(NewSpecOpenApi); + var paths = + PathsDiff.Diff(PathsDiff.ValOrEmpty(OldSpecOpenApi.Paths), PathsDiff.ValOrEmpty(NewSpecOpenApi.Paths)); + NewEndpoints = new List(); + MissingEndpoints = new List(); + ChangedOperations = new List(); + + if (paths != null) + { + NewEndpoints = EndpointUtils.ConvertToEndpointList(paths.Increased); + MissingEndpoints = EndpointUtils.ConvertToEndpointList(paths.Missing); + foreach (var (key, value) in paths.Changed) + { + NewEndpoints.AddRange(EndpointUtils.ConvertToEndpoints(key, value.Increased)); + MissingEndpoints.AddRange(EndpointUtils.ConvertToEndpoints(key, value.Missing)); + ChangedOperations.AddRange(value.Changed); + } + } + + var diff = ExtensionsDiff + .Diff(OldSpecOpenApi.Extensions, NewSpecOpenApi.Extensions); + + if (diff != null) + ChangedExtensions = diff; + return GetChangedOpenApi(); + } + + private static void PreProcess(OpenApiDocument openApi) + { + var securityRequirements = openApi.SecurityRequirements; + + if (securityRequirements != null) + { + var distinctSecurityRequirements = + securityRequirements.Distinct().ToList(); + var paths = openApi.Paths; + if (paths != null) + { + foreach (var openApiPathItem in paths.Values) + { + var operationsWithSecurity = openApiPathItem + .Operations + .Values + .Where(x => !x.Security.IsNullOrEmpty()); + foreach (var openApiOperation in operationsWithSecurity) + { + openApiOperation.Security = openApiOperation.Security.Distinct().ToList(); + } + var operationsWithoutSecurity = openApiPathItem + .Operations + .Values + .Where(x => x.Security.IsNullOrEmpty()); + foreach (var openApiOperation in operationsWithoutSecurity) + { + openApiOperation.Security = distinctSecurityRequirements; + } + } + } + + openApi.SecurityRequirements = null; + } + } + + private ChangedOpenApiBO GetChangedOpenApi() + { + return new ChangedOpenApiBO(OldIdentifier, NewIdentifier) + { + MissingEndpoints = MissingEndpoints, + NewEndpoints = NewEndpoints, + NewSpecOpenApi = NewSpecOpenApi, + OldSpecOpenApi = OldSpecOpenApi, + ChangedOperations = ChangedOperations, + ChangedExtensions = ChangedExtensions + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs new file mode 100644 index 000000000..6c6cd4d4b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/OperationDiff.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class OperationDiff + { + private readonly OpenApiDiff _openApiDiff; + + public OperationDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedOperationBO Diff( + OpenApiOperation oldOperation, OpenApiOperation newOperation, DiffContextBO context) + { + var changedOperation = + new ChangedOperationBO(context.URL, context.Method, oldOperation, newOperation) + { + Summary = _openApiDiff + .MetadataDiff + .Diff(oldOperation.Summary, newOperation.Summary, context), + Description = _openApiDiff + .MetadataDiff + .Diff(oldOperation.Description, newOperation.Description, context), + IsDeprecated = !oldOperation.Deprecated && newOperation.Deprecated + }; + + if (oldOperation.RequestBody != null || newOperation.RequestBody != null) + changedOperation.RequestBody = _openApiDiff + .RequestBodyDiff + .Diff( + oldOperation.RequestBody, newOperation.RequestBody, context.CopyAsRequest()); + + var parametersDiff = _openApiDiff + .ParametersDiff + .Diff(oldOperation.Parameters.ToList(), newOperation.Parameters.ToList(), context); + + if (parametersDiff != null) + { + RemovePathParameters(context.Parameters, parametersDiff); + changedOperation.Parameters = parametersDiff; + } + + + if (oldOperation.Responses != null || newOperation.Responses != null) + { + + var diff = _openApiDiff + .APIResponseDiff + .Diff(oldOperation.Responses, newOperation.Responses, context.CopyAsResponse()); + + if (diff != null) + changedOperation.APIResponses = diff; + } + + if (oldOperation.Security != null || newOperation.Security != null) + { + var diff = _openApiDiff + .SecurityRequirementsDiff + .Diff(oldOperation.Security, newOperation.Security, context); + + if (diff != null) + changedOperation.SecurityRequirements = diff; + } + + changedOperation.Extensions = + _openApiDiff + .ExtensionsDiff + .Diff(oldOperation.Extensions, newOperation.Extensions, context); + + return ChangedUtils.IsChanged(changedOperation); + } + + public void RemovePathParameters(Dictionary pathParameters, ChangedParametersBO parameters) + { + foreach (var (oldParam, newParam) in pathParameters) + { + RemovePathParameter(oldParam, parameters.Missing); + RemovePathParameter(newParam, parameters.Increased); + } + } + + public void RemovePathParameter(string name, List parameters) + { + var openApiParameters = parameters + .FirstOrDefault(x => x.In == ParameterLocation.Path && x.Name == name); + if (!parameters.IsNullOrEmpty() && openApiParameters != null) + parameters.Remove(openApiParameters); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs new file mode 100644 index 000000000..186df4688 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ParameterDiff.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ParameterDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Parameters); + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + + public ParameterDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedParameterBO Diff(OpenApiParameter left, OpenApiParameter right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedParameterBO ComputeDiff(HashSet refSet, OpenApiParameter left, OpenApiParameter right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedParameter = + new ChangedParameterBO(right.Name, right.In, left, right, context) + { + IsChangeRequired = GetBooleanDiff(left.Required, right.Required), + IsDeprecated = !left.Deprecated && right.Deprecated, + ChangeAllowEmptyValue = GetBooleanDiff(left.AllowEmptyValue, right.AllowEmptyValue), + ChangeStyle = left.Style != right.Style, + ChangeExplode = GetBooleanDiff(left.Explode, right.Explode), + Schema = _openApiDiff + .SchemaDiff + .Diff(refSet, left.Schema, right.Schema, context.CopyWithRequired(true)), + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedParameter); + } + + private static bool GetBooleanDiff(bool? left, bool? right) + { + var leftRequired = left ?? false; + var rightRequired = right ?? false; + return leftRequired != rightRequired; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs new file mode 100644 index 000000000..308f880a8 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ParametersDiff.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ParametersDiff + { + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Parameters); + + public ParametersDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public static OpenApiParameter Contains(OpenApiComponents components, List parameters, OpenApiParameter parameter) + { + return parameters + .FirstOrDefault(x => + Same(RefPointer.ResolveRef(components, x, x.Reference?.ReferenceV3), parameter)); + } + + public static bool Same(OpenApiParameter left, OpenApiParameter right) + { + return left.Name == right.Name && left.In.Equals(right.In); + } + + public ChangedParametersBO Diff( + List left, List right, DiffContextBO context) + { + var changedParameters = + new ChangedParametersBO(left, right, context); + if (null == left) left = new List(); + if (null == right) right = new List(); + + foreach (var openApiParameter in left) + { + var leftPara = openApiParameter; + leftPara = RefPointer.ResolveRef(_leftComponents, leftPara, leftPara.Reference?.ReferenceV3); + + var rightParam = Contains(_rightComponents, right, leftPara); + if (rightParam == null) + { + changedParameters.Missing.Add(leftPara); + } + else + { + right.Remove(rightParam); + + var diff = _openApiDiff.ParameterDiff + .Diff(leftPara, rightParam, context); + if (diff != null) + changedParameters.Changed.Add(diff); + } + } + + changedParameters.Increased.AddRange(right); + + return ChangedUtils.IsChanged(changedParameters); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs new file mode 100644 index 000000000..b1d27920f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/PathDiff.cs @@ -0,0 +1,48 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class PathDiff + { + private readonly OpenApiDiff _openApiDiff; + + public PathDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedPathBO Diff(OpenApiPathItem left, OpenApiPathItem right, DiffContextBO context) + { + var oldOperationMap = left.Operations; + var newOperationMap = right.Operations; + var operationsDiff = + MapKeyDiff.Diff(oldOperationMap, newOperationMap); + var sharedMethods = operationsDiff.SharedKey; + var changedPath = new ChangedPathBO(context.URL, left, right, context) + { + Increased = operationsDiff.Increased, + Missing = operationsDiff.Missing + }; + foreach (var operationType in sharedMethods) + { + var oldOperation = oldOperationMap[operationType]; + var newOperation = newOperationMap[operationType]; + + var diff = _openApiDiff + .OperationDiff + .Diff(oldOperation, newOperation, context.CopyWithMethod(operationType)); + + if (diff != null) + changedPath.Changed.Add(diff); + } + + changedPath.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context); + + return ChangedUtils.IsChanged(changedPath); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs new file mode 100644 index 000000000..0937f06bd --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/PathsDiff.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class PathsDiff + { + private readonly OpenApiDiff _openApiDiff; + + public PathsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + public ChangedPathsBO Diff(Dictionary left, Dictionary right) + { + var changedPaths = new ChangedPathsBO(left, right); + + foreach (var (key, value) in right) + { + changedPaths.Increased.Add(key, value); + } + + foreach (var (key, value) in left) + { + var template = key.NormalizePath(); + var result = right.Keys.FirstOrDefault(x => x.NormalizePath() == template); + + if (result != null) + { + if (!changedPaths.Increased.ContainsKey(result)) + throw new ArgumentException($"Two path items have the same signature: {template}"); + var rightPath = changedPaths.Increased[result]; + changedPaths.Increased.Remove(result); + var paramsDict = new Dictionary(); + if (key != result) + { + var oldParams = key.ExtractParametersFromPath(); + var newParams = result.ExtractParametersFromPath(); + for (var i = oldParams.Count - 1; i >= 0; i--) + { + paramsDict.Add(oldParams[i], newParams[i]); + } + } + var context = new DiffContextBO() + { + URL = key, + Parameters = paramsDict + }; + + var diff = _openApiDiff + .PathDiff + .Diff(value, rightPath, context); + + if (diff != null) + changedPaths.Changed.Add(result, diff); + } + else + { + changedPaths.Missing.Add(key, value); + } + } + + return ChangedUtils.IsChanged(changedPaths); + } + + public static OpenApiPaths ValOrEmpty(OpenApiPaths path) + { + return path ?? new OpenApiPaths(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs b/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs new file mode 100644 index 000000000..d7a9f5b9b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ReferenceDiffCache.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public abstract class ReferenceDiffCache + where TD : class + { + public Dictionary RefDiffMap { get; set; } + + protected ReferenceDiffCache() + { + RefDiffMap = new Dictionary(); + } + + protected string GetRefKey(string leftRef, string rightRef) + { + return leftRef + ":" + rightRef; + } + + protected abstract TD ComputeDiff( + HashSet refSet, TC left, TC right, DiffContextBO context); + + public TD CachedDiff( + HashSet refSet, + TC left, + TC right, + string leftRef, + string rightRef, + DiffContextBO context) + { + var areBothRefParameters = leftRef != null && rightRef != null; + if (areBothRefParameters) + { + var key = new CacheKey(leftRef, rightRef, context); + if (RefDiffMap.TryGetValue(key, out var changedFromRef)) + return changedFromRef; + + var refKey = GetRefKey(leftRef, rightRef); + if (refSet.Contains(refKey)) + return null; + + refSet.Add(refKey); + var changed = ComputeDiff(refSet, left, right, context); + RefDiffMap.Add(key, changed); + refSet.Remove(refKey); + return changed; + } + + return ComputeDiff(refSet, left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs new file mode 100644 index 000000000..e5e8a2712 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/RequestBodyDiff.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class RequestBodyDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.RequestBodies); + private readonly OpenApiDiff _openApiDiff; + + public RequestBodyDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + } + + private static IDictionary GetExtensions(OpenApiRequestBody body) + { + return body.Extensions.ToDictionary(x => x.Key, x => x.Value); + } + + public ChangedRequestBodyBO Diff( + OpenApiRequestBody left, OpenApiRequestBody right, DiffContextBO context) + { + var leftRef = left.Reference?.ReferenceV3; + var rightRef = right.Reference?.ReferenceV3; + return CachedDiff(new HashSet(), left, right, leftRef, rightRef, context); + } + + protected override ChangedRequestBodyBO ComputeDiff(HashSet refSet, OpenApiRequestBody left, OpenApiRequestBody right, + DiffContextBO context) + { + Dictionary oldRequestContent = null; + Dictionary newRequestContent = null; + OpenApiRequestBody oldRequestBody = null; + OpenApiRequestBody newRequestBody = null; + if (left != null) + { + oldRequestBody = + RefPointer.ResolveRef( + _openApiDiff.OldSpecOpenApi.Components, left, left.Reference?.ReferenceV3); + if (oldRequestBody.Content != null) + { + oldRequestContent = (Dictionary) oldRequestBody.Content; + } + } + if (right != null) + { + newRequestBody = + RefPointer.ResolveRef( + _openApiDiff.NewSpecOpenApi.Components, right, right.Reference?.ReferenceV3); + if (newRequestBody.Content != null) + { + newRequestContent = (Dictionary) newRequestBody.Content; + } + } + var leftRequired = + oldRequestBody != null && oldRequestBody.Required; + var rightRequired = + newRequestBody != null && newRequestBody.Required; + + var changedRequestBody = + new ChangedRequestBodyBO(oldRequestBody, newRequestBody, context) + { + ChangeRequired = leftRequired != rightRequired, + Description = _openApiDiff + .MetadataDiff + .Diff( + oldRequestBody?.Description, + newRequestBody?.Description, + context), + Content = _openApiDiff + .ContentDiff + .Diff(oldRequestContent, newRequestContent, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(GetExtensions(left), GetExtensions(right), context) + }; + + return ChangedUtils.IsChanged(changedRequestBody); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs new file mode 100644 index 000000000..436551a3e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/ResponseDiff.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class ResponseDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Responses); + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public ResponseDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedResponseBO Diff(OpenApiResponse left, OpenApiResponse right, DiffContextBO context) + { + return CachedDiff(new HashSet(), left, right, left.Reference?.ReferenceV3, right.Reference?.ReferenceV3, context); + } + + protected override ChangedResponseBO ComputeDiff(HashSet refSet, OpenApiResponse left, OpenApiResponse right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, left.Reference?.ReferenceV3); + right = RefPointer.ResolveRef(_rightComponents, right, right.Reference?.ReferenceV3); + + var changedResponse = new ChangedResponseBO(left, right, context) + { + Description = _openApiDiff + .MetadataDiff + .Diff(left.Description, right.Description, context), + Content = _openApiDiff + .ContentDiff + .Diff(left.Content, right.Content, context), + Headers = _openApiDiff + .HeadersDiff + .Diff(left.Headers, right.Headers, context), + Extensions = _openApiDiff + .ExtensionsDiff + .Diff(left.Extensions, right.Extensions, context) + }; + + return ChangedUtils.IsChanged(changedResponse); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs new file mode 100644 index 000000000..d29745b0b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiff.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Compare.SchemaDiffResult; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SchemaDiff : ReferenceDiffCache + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Schemas); + + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private readonly OpenApiDiff _openApiDiff; + + public SchemaDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public static SchemaDiffResult.SchemaDiffResult GetSchemaDiffResult(OpenApiDiff openApiDiff) + { + return GetSchemaDiffResult(null, openApiDiff); + } + + public static SchemaDiffResult.SchemaDiffResult GetSchemaDiffResult(OpenApiSchema schema, OpenApiDiff openApiDiff) + { + switch (schema.GetSchemaType()) + { + case SchemaTypeEnum.Schema: + return new SchemaDiffResult.SchemaDiffResult(openApiDiff); + case SchemaTypeEnum.ArraySchema: + return new ArraySchemaDiffResult(openApiDiff); + case SchemaTypeEnum.ComposedSchema: + return new ComposedSchemaDiffResult(openApiDiff); + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected static OpenApiSchema ResolveComposedSchema(OpenApiComponents components, OpenApiSchema schema) + { + if (schema != null && schema.GetSchemaType() == SchemaTypeEnum.ComposedSchema) + { + var allOfSchemaList = schema.AllOf; + if (!allOfSchemaList.IsNullOrEmpty()) + { + var refName = "allOfCombined-"; + allOfSchemaList + .ToList() + .ForEach(x => refName += x.Reference?.ReferenceV3); + if (components.Schemas.ContainsKey(refName)) + return components.Schemas[refName]; + components.Schemas.Add(refName, new OpenApiSchema()); + + var allOfCombinedSchema = new OpenApiSchema(); + allOfCombinedSchema = AddSchema(allOfCombinedSchema, schema); + foreach (var t in allOfSchemaList) + { + var allOfSchema = t; + allOfSchema = + RefPointer.ResolveRef(components, allOfSchema, allOfSchema.Reference?.ReferenceV3); + allOfSchema = ResolveComposedSchema(components, allOfSchema); + allOfCombinedSchema = AddSchema(allOfCombinedSchema, allOfSchema); + } + return allOfCombinedSchema; + } + } + return schema; + } + + protected static OpenApiSchema AddSchema(OpenApiSchema schema, OpenApiSchema fromSchema) + { + if (fromSchema.Properties != null) + { + if (schema.Properties == null) + { + schema.Properties = new Dictionary(); + } + + foreach (var property in fromSchema.Properties) + { + schema.Properties.Add(property); + } + } + + if (fromSchema.Required != null) + { + if (schema.Required == null) + { + schema.Required = fromSchema.Required; + } + else + { + foreach (var required in fromSchema.Required) + { + schema.Required.Add(required); + } + } + } + + schema.ReadOnly = fromSchema.ReadOnly; + schema.WriteOnly = fromSchema.WriteOnly; + schema.Deprecated = fromSchema.Deprecated; + schema.Nullable = fromSchema.Nullable; + + if (fromSchema.ExclusiveMaximum != null) + { + schema.ExclusiveMaximum = fromSchema.ExclusiveMaximum; + } + if (fromSchema.ExclusiveMinimum != null) + { + schema.ExclusiveMinimum = fromSchema.ExclusiveMinimum; + } + if (fromSchema.UniqueItems != null) + { + schema.UniqueItems = fromSchema.UniqueItems; + } + if (fromSchema.Description != null) + { + schema.Description = fromSchema.Description; + } + if (fromSchema.Format != null) + { + schema.Format = fromSchema.Format; + } + if (fromSchema.Type != null) + { + schema.Type = fromSchema.Type; + } + if (fromSchema.Enum != null) + { + if (schema.Enum == null) + { + schema.Enum = new List(); + } + //noinspection unchecked + foreach (var element in fromSchema.Enum) + { + schema.Enum.Add(element); + } + } + if (fromSchema.Extensions != null) + { + if (schema.Extensions == null) + { + schema.Extensions = new Dictionary(); + } + foreach (var element in fromSchema.Extensions) + { + schema.Extensions.Add(element); + } + } + if (fromSchema.Discriminator != null) + { + if (schema.Discriminator == null) + { + schema.Discriminator = new OpenApiDiscriminator(); + } + var discriminator = schema.Discriminator; + var fromDiscriminator = fromSchema.Discriminator; + + if (fromDiscriminator.PropertyName != null) + { + discriminator.PropertyName = fromDiscriminator.PropertyName; + } + if (fromDiscriminator.Mapping != null) + { + if (discriminator.Mapping == null) + { + discriminator.Mapping = new Dictionary(); + } + foreach (var element in fromDiscriminator.Mapping) + { + discriminator.Mapping.Add(element); + } + } + } + if (fromSchema.Title != null) + { + schema.Title = fromSchema.Title; + } + if (fromSchema.AdditionalProperties != null) + { + schema.AdditionalProperties = fromSchema.AdditionalProperties; + } + if (fromSchema.Default != null) + { + schema.Default = fromSchema.Default; + } + if (fromSchema.Example != null) + { + schema.Example = fromSchema.Example; + } + if (fromSchema.ExternalDocs != null) + { + if (schema.ExternalDocs == null) + { + schema.ExternalDocs = new OpenApiExternalDocs(); + } + var externalDocs = schema.ExternalDocs; + var fromExternalDocs = fromSchema.ExternalDocs; + if (fromExternalDocs.Description != null) + { + externalDocs.Description = fromExternalDocs.Description; + } + if (fromExternalDocs.Extensions != null) + { + if (externalDocs.Extensions == null) + { + externalDocs.Extensions = new Dictionary(); + } + + foreach (var element in fromSchema.Extensions) + { + schema.Extensions.Add(element); + } + } + if (fromExternalDocs.Url != null) + { + externalDocs.Url = fromExternalDocs.Url; + } + } + if (fromSchema.Maximum != null) + { + schema.Maximum = fromSchema.Maximum; + } + if (fromSchema.Minimum != null) + { + schema.Minimum = fromSchema.Minimum; + } + if (fromSchema.MaxItems != null) + { + schema.MaxItems = fromSchema.MaxItems; + } + if (fromSchema.MinItems != null) + { + schema.MinItems = fromSchema.MinItems; + } + if (fromSchema.MaxProperties != null) + { + schema.MaxProperties = fromSchema.MaxProperties; + } + if (fromSchema.MinProperties != null) + { + schema.MinProperties = fromSchema.MinProperties; + } + if (fromSchema.MaxLength != null) + { + schema.MaxLength = fromSchema.MaxLength; + } + if (fromSchema.MinLength != null) + { + schema.MinLength = fromSchema.MinLength; + } + if (fromSchema.MultipleOf != null) + { + schema.MultipleOf = fromSchema.MultipleOf; + } + if (fromSchema.Not != null) + { + if (schema.Not == null) + { + schema.Not = AddSchema(new OpenApiSchema(), fromSchema.Not); + } + else + { + AddSchema(schema.Not, fromSchema.Not); + } + } + if (fromSchema.Pattern != null) + { + schema.Pattern = fromSchema.Pattern; + } + if (fromSchema.Xml != null) + { + if (schema.Xml == null) + { + schema.Xml = new OpenApiXml(); + } + var xml = schema.Xml; + var fromXml = fromSchema.Xml; + + xml.Attribute = fromXml.Attribute; + + if (fromXml.Name != null) + { + xml.Name = fromXml.Name; + } + if (fromXml.Namespace != null) + { + xml.Namespace = fromXml.Namespace; + } + if (fromXml.Extensions != null) + { + if (xml.Extensions == null) + { + xml.Extensions = new Dictionary(); + } + foreach (var element in fromXml.Extensions) + { + xml.Extensions.Add(element); + } + } + if (fromXml.Prefix != null) + { + xml.Prefix = fromXml.Prefix; + } + + xml.Wrapped = fromXml.Wrapped; + } + return schema; + } + + private static string GetSchemaRef(OpenApiSchema schema) + { + return schema?.Reference?.ReferenceV3; + } + + public ChangedSchemaBO Diff(HashSet refSet, OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + if (left == null && right == null) + { + return null; + } + return CachedDiff(refSet, left, right, GetSchemaRef(left), GetSchemaRef(right), context); + } + + public ChangedSchemaBO GetTypeChangedSchema( + OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + var schemaDiffResult = GetSchemaDiffResult(_openApiDiff); + schemaDiffResult.ChangedSchema.OldSchema = left; + schemaDiffResult.ChangedSchema.NewSchema = right; + schemaDiffResult.ChangedSchema.IsChangedType = true; + schemaDiffResult.ChangedSchema.Context = context; + + return schemaDiffResult.ChangedSchema; + } + + protected override ChangedSchemaBO ComputeDiff( + HashSet refSet, OpenApiSchema left, OpenApiSchema right, DiffContextBO context) + { + left = RefPointer.ResolveRef(_leftComponents, left, GetSchemaRef(left)); + right = RefPointer.ResolveRef(_rightComponents, right, GetSchemaRef(right)); + + left = ResolveComposedSchema(_leftComponents, left); + right = ResolveComposedSchema(_rightComponents, right); + + // If type of schemas are different, just set old & new schema, set changedType to true in + // SchemaDiffResult and + // return the object + if ((left == null || right == null) + || left.Type != right.Type + || left.Format != right.Format) + { + return GetTypeChangedSchema(left, right, context); + } + + // If schema type is same then get specific SchemaDiffResult and compare the properties + var result = GetSchemaDiffResult(right, _openApiDiff); + return result.Diff(refSet, _leftComponents, _rightComponents, left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs new file mode 100644 index 000000000..3351bc071 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ArraySchemaDiffResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class ArraySchemaDiffResult : SchemaDiffResult + { + public ArraySchemaDiffResult(OpenApiDiff openApiDiff) : base("array", openApiDiff) + { + } + + public override ChangedSchemaBO Diff(HashSet refSet, OpenApiComponents leftComponents, OpenApiComponents rightComponents, T left, + T right, DiffContextBO context) + { + if (left.GetSchemaType() != SchemaTypeEnum.ArraySchema + || right.GetSchemaType() != SchemaTypeEnum.ArraySchema) + return null; + + base.Diff(refSet, leftComponents, rightComponents, left, right, context); + + var diff = OpenApiDiff + .SchemaDiff + .Diff( + refSet, + left.Items, + right.Items, + context.CopyWithRequired(true)); + if (diff != null) + ChangedSchema.Items = diff; + + return IsApplicable(context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs new file mode 100644 index 000000000..8343604a6 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/ComposedSchemaDiffResult.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class ComposedSchemaDiffResult : SchemaDiffResult + { + private static RefPointer refPointer = new RefPointer(RefTypeEnum.Schemas); + + public ComposedSchemaDiffResult(OpenApiDiff openApiDiff) : base(openApiDiff) + { + } + + private static Dictionary GetSchema(OpenApiComponents components, Dictionary mapping) + { + var result = new Dictionary(); + foreach (var map in mapping) + { + result.Add(map.Key, refPointer.ResolveRef(components, new OpenApiSchema(), map.Value)); + } + return result; + } + + private static Dictionary GetMapping(OpenApiSchema composedSchema) + { + if (composedSchema.GetSchemaType() != SchemaTypeEnum.ComposedSchema) + return null; + + var reverseMapping = new Dictionary(); + foreach (var schema in composedSchema.OneOf) + { + var schemaRef = schema.Reference?.ReferenceV3; + if (schemaRef == null) + { + throw new ArgumentNullException("invalid oneOf schema"); + } + var schemaName = refPointer.GetRefName(schemaRef); + if (schemaName == null) + { + throw new ArgumentNullException("invalid schema: " + schemaRef); + } + reverseMapping.Add(schemaRef, schemaName); + } + + if (!composedSchema.Discriminator.Mapping.IsNullOrEmpty()) + { + foreach (var (key, value) in composedSchema.Discriminator.Mapping) + { + if (!reverseMapping.TryAdd(value, key)) + reverseMapping[value] = key; + } + } + + return reverseMapping.ToDictionary(x => x.Value, x => x.Key); + } + + public override ChangedSchemaBO Diff(HashSet refSet, OpenApiComponents leftComponents, OpenApiComponents rightComponents, T left, + T right, DiffContextBO context) + { + if (left.GetSchemaType() == SchemaTypeEnum.ComposedSchema) + { + if (!left.OneOf.IsNullOrEmpty() || !right.OneOf.IsNullOrEmpty()) + { + var leftDis = left.Discriminator; + var rightDis = right.Discriminator; + if (leftDis == null + || rightDis == null + || leftDis.PropertyName == null + || rightDis.PropertyName == null) + { + throw new ArgumentException( + "discriminator or property not found for oneOf schema"); + } + + if (leftDis.PropertyName != rightDis.PropertyName + || left.OneOf.IsNullOrEmpty() + || right.OneOf.IsNullOrEmpty()) + { + ChangedSchema.OldSchema = left; + ChangedSchema.NewSchema = right; + ChangedSchema.DiscriminatorPropertyChanged = true; + ChangedSchema.Context = context; + return ChangedSchema; + } + + var leftMapping = GetMapping(left); + var rightMapping = GetMapping(right); + + var mappingDiff = MapKeyDiff.Diff(GetSchema(leftComponents, leftMapping), GetSchema(rightComponents, rightMapping)); + var changedMapping = new Dictionary(); + foreach (var refId in mappingDiff.SharedKey) + { + var leftReference = leftComponents.Schemas.Values + .First(x => x.Reference.ReferenceV3 == leftMapping[refId]).Reference; + var rightReference = rightComponents.Schemas.Values + .First(x => x.Reference.ReferenceV3 == rightMapping[refId]).Reference; + + var leftSchema = new OpenApiSchema { Reference = leftReference }; + var rightSchema = new OpenApiSchema { Reference = rightReference }; + var changedSchema = OpenApiDiff.SchemaDiff + .Diff(refSet, leftSchema, rightSchema, context.CopyWithRequired(true)); + if (changedSchema != null) + changedMapping.Add(refId, changedSchema); + } + + ChangedSchema.OneOfSchema = new ChangedOneOfSchemaBO(leftMapping, rightMapping, context) + { + Increased = mappingDiff.Increased, + Missing = mappingDiff.Missing, + Changed = changedMapping + }; + } + return base.Diff(refSet, leftComponents, rightComponents, left, right, context); + } + + return OpenApiDiff.SchemaDiff.GetTypeChangedSchema(left, right, context); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs new file mode 100644 index 000000000..c1290fb39 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SchemaDiffResult/SchemaDiffResult.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare.SchemaDiffResult +{ + public class SchemaDiffResult + { + public ChangedSchemaBO ChangedSchema { get; set; } + public OpenApiDiff OpenApiDiff { get; set; } + + public SchemaDiffResult(OpenApiDiff openApiDiff) + { + OpenApiDiff = openApiDiff; + ChangedSchema = new ChangedSchemaBO(); + } + + public SchemaDiffResult(string type, OpenApiDiff openApiDiff) : this(openApiDiff) + { + ChangedSchema.Type = type; + } + + public virtual ChangedSchemaBO Diff( + HashSet refSet, + OpenApiComponents leftComponents, + OpenApiComponents rightComponents, + T left, + T right, + DiffContextBO context) + where T : OpenApiSchema + { + var leftEnumStrings = left.Enum.Select(x => ((IOpenApiPrimitive)x)?.GetValueString()).ToList(); + var rightEnumStrings = right.Enum.Select(x => ((IOpenApiPrimitive)x)?.GetValueString()).ToList(); + var leftDefault= (IOpenApiPrimitive)left.Default; + var rightDefault = (IOpenApiPrimitive)right.Default; + + var changedEnum = + ListDiff.Diff(new ChangedEnumBO(leftEnumStrings, rightEnumStrings, context)); + + ChangedSchema.Context = context; + ChangedSchema.OldSchema = left; + ChangedSchema.NewSchema = right; + ChangedSchema.IsChangeDeprecated = !left.Deprecated && right.Deprecated; + ChangedSchema.IsChangeTitle = left.Title != right.Title; + ChangedSchema.Required = ListDiff.Diff(new ChangedRequiredBO(left.Required.ToList(), right.Required.ToList(), context)); + ChangedSchema.IsChangeDefault = leftDefault?.GetValueString() != rightDefault?.GetValueString(); + ChangedSchema.Enumeration = changedEnum; + ChangedSchema.IsChangeFormat = left.Format != right.Format; + ChangedSchema.ReadOnly = new ChangedReadOnlyBO(left.ReadOnly, right.ReadOnly, context); + ChangedSchema.WriteOnly = new ChangedWriteOnlyBO(left.WriteOnly, right.WriteOnly, context); + ChangedSchema.MaxLength = new ChangedMaxLengthBO(left.MaxLength, right.MaxLength, context); + + var extendedDiff = OpenApiDiff.ExtensionsDiff.Diff(left.Extensions, right.Extensions, context); + if (extendedDiff != null) + ChangedSchema.Extensions = extendedDiff; + var metaDataDiff = OpenApiDiff.MetadataDiff.Diff(left.Description, right.Description, context); + if (metaDataDiff != null) + ChangedSchema.Description = metaDataDiff; + + var leftProperties = left.Properties; + var rightProperties = right.Properties; + var propertyDiff = MapKeyDiff.Diff(leftProperties, rightProperties); + + foreach (var s in propertyDiff.SharedKey) + { + var diff = OpenApiDiff + .SchemaDiff + .Diff(refSet, leftProperties[s], rightProperties[s], Required(context, s, right.Required)); + + if (diff != null) + ChangedSchema.ChangedProperties.Add(s, diff); + } + + CompareAdditionalProperties(refSet, left, right, context); + + var allIncreasedProperties = FilterProperties(TypeEnum.Added, propertyDiff.Increased, context); + foreach (var (key, value) in allIncreasedProperties) + { + ChangedSchema.IncreasedProperties.Add(key, value); + } + var allMissingProperties = FilterProperties(TypeEnum.Removed, propertyDiff.Missing, context); + foreach (var (key, value) in allMissingProperties) + { + ChangedSchema.MissingProperties.Add(key, value); + } + + return IsApplicable(context); + } + + private static DiffContextBO Required(DiffContextBO context, string key, ICollection required) + { + return context.CopyWithRequired(required != null && required.Contains(key)); + } + + private void CompareAdditionalProperties(HashSet refSet, OpenApiSchema leftSchema, OpenApiSchema rightSchema, DiffContextBO context) + { + var left = leftSchema.AdditionalProperties; + var right = rightSchema.AdditionalProperties; + if (left != null || right != null) + { + var apChangedSchema = new ChangedSchemaBO + { + Context = context, + OldSchema = left, + NewSchema = right + }; + if (left != null && right != null) + { + var addPropChangedSchemaOp = + OpenApiDiff + .SchemaDiff + .Diff(refSet, left, right, context.CopyWithRequired(false)); + apChangedSchema = addPropChangedSchemaOp ?? apChangedSchema; + } + var changed = ChangedUtils.IsChanged(apChangedSchema); + if (changed != null) + ChangedSchema.AddProp = changed; + } + } + private Dictionary FilterProperties(TypeEnum type, Dictionary properties, DiffContextBO context) + { + var result = new Dictionary(); + + foreach (var (key, value) in properties) + { + if (IsPropertyApplicable(value, context) + && OpenApiDiff + .ExtensionsDiff.IsParentApplicable(type, + value, + value?.Extensions ?? new Dictionary(), + context)) + { + result.Add(key, value); + } + else + { + // Child property is not applicable, so required cannot be applied + ChangedSchema.Required.Increased.Remove(key); + } + } + + + return result; + } + + private static bool IsPropertyApplicable(OpenApiSchema schema, DiffContextBO context) + { + return !(context.IsResponse && schema.WriteOnly) && !(context.IsRequest && schema.ReadOnly); + } + + protected ChangedSchemaBO IsApplicable(DiffContextBO context) + { + if (ChangedSchema.ReadOnly.IsUnchanged() + && ChangedSchema.WriteOnly.IsUnchanged() + && !IsPropertyApplicable(ChangedSchema.NewSchema, context)) + { + return null; + } + return ChangedUtils.IsChanged(ChangedSchema); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs new file mode 100644 index 000000000..17a5c018b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementDiff.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecurityRequirementDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public SecurityRequirementDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + public static OpenApiSecurityRequirement GetCopy(Dictionary> right) + { + var newSecurityRequirement = new OpenApiSecurityRequirement(); + foreach (var (key, value) in right) + { + newSecurityRequirement.Add(key, value); + } + + return newSecurityRequirement; + } + + private OpenApiSecurityRequirement Contains( + OpenApiSecurityRequirement right, string schemeRef) + { + var leftSecurityScheme = _leftComponents.SecuritySchemes[schemeRef]; + var found = new OpenApiSecurityRequirement(); + + foreach (var keyValuePair in right) + { + var rightSecurityScheme = _rightComponents.SecuritySchemes[keyValuePair.Key.Reference?.ReferenceV3]; + if (leftSecurityScheme.Type == rightSecurityScheme.Type) + { + switch (leftSecurityScheme.Type) + { + case SecuritySchemeType.ApiKey: + if (leftSecurityScheme.Name == rightSecurityScheme.Name) + { + found.Add(keyValuePair.Key, keyValuePair.Value); + return found; + } + + break; + case SecuritySchemeType.Http: + case SecuritySchemeType.OAuth2: + case SecuritySchemeType.OpenIdConnect: + found.Add(keyValuePair.Key, keyValuePair.Value); + return found; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + return found; + } + + public ChangedSecurityRequirementBO Diff( + OpenApiSecurityRequirement left, OpenApiSecurityRequirement right, DiffContextBO context) + { + var changedSecurityRequirement = + new ChangedSecurityRequirementBO(left, right != null ? GetCopy(right) : null); + + left ??= new OpenApiSecurityRequirement(); + right ??= new OpenApiSecurityRequirement(); + + foreach (var (key, value) in left) + { + var rightSec = Contains(right, key.Reference?.ReferenceV3); + if (rightSec.IsNullOrEmpty()) + { + changedSecurityRequirement.Missing.Add(key, value); + } + else + { + var rightSchemeRef = rightSec.Keys.First(); + right.Remove(rightSchemeRef); + var diff = + _openApiDiff + .SecuritySchemeDiff + .Diff( + key.Reference?.ReferenceV3, + value.ToList(), + rightSchemeRef.Reference?.ReferenceV3, + rightSec[rightSchemeRef].ToList(), + context); + if (diff != null) + changedSecurityRequirement.Changed.Add(diff); + } + } + + foreach (var (key, value) in right) + { + changedSecurityRequirement.Increased.Add(key, value); + } + + return ChangedUtils.IsChanged(changedSecurityRequirement); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs new file mode 100644 index 000000000..c54c8197d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecurityRequirementsDiff.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecurityRequirementsDiff + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + private static RefPointer _refPointer = new RefPointer(RefTypeEnum.SecuritySchemes); + + public SecurityRequirementsDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + public OpenApiSecurityRequirement Contains(IList securityRequirements, OpenApiSecurityRequirement left) + { + return securityRequirements + .FirstOrDefault(x => Same(left, x)); + } + + public bool Same(OpenApiSecurityRequirement left, OpenApiSecurityRequirement right) + { + var leftTypes = GetListOfSecuritySchemes(_leftComponents, left); + var rightTypes = GetListOfSecuritySchemes(_rightComponents, right); + + return leftTypes.SequenceEqual(rightTypes); + } + + private static ImmutableDictionary GetListOfSecuritySchemes( + OpenApiComponents components, OpenApiSecurityRequirement securityRequirement) + { + var tmpResult = new Dictionary(); + foreach (var openApiSecurityScheme in securityRequirement.Keys.ToList()) + { + + if (components.SecuritySchemes.TryGetValue(openApiSecurityScheme.Reference?.ReferenceV3, out var result)) + { + if (!tmpResult.ContainsKey(result.Type)) + tmpResult.Add(result.Type, result.In); + } + else + { + throw new ArgumentException("Impossible to find security scheme: " + openApiSecurityScheme.Scheme); + } + } + return tmpResult.ToImmutableDictionary(); + } + + public ChangedSecurityRequirementsBO Diff( + IList left, IList right, DiffContextBO context) + { + left ??= new List(); + right = right != null ? GetCopy(right) : new List(); + + var changedSecurityRequirements = new ChangedSecurityRequirementsBO(left, right); + + foreach (var leftSecurity in left) + { + var rightSecOpt = Contains(right, leftSecurity); + if (rightSecOpt == null) + { + changedSecurityRequirements.Missing.Add(leftSecurity); + } + else + { + var rightSec = rightSecOpt; + + right.Remove(rightSec); + var diff = + _openApiDiff. + SecurityRequirementDiff + .Diff(leftSecurity, rightSec, context); + if (diff != null) + changedSecurityRequirements.Changed.Add(diff); + } + } + + changedSecurityRequirements.Increased.AddRange(right); + + return ChangedUtils.IsChanged(changedSecurityRequirements); + } + + private static List GetCopy(IEnumerable right) + { + return right.Select(SecurityRequirementDiff.GetCopy).ToList(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs b/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs new file mode 100644 index 000000000..5906767bc --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Compare/SecuritySchemeDiff.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Compare +{ + public class SecuritySchemeDiff : ReferenceDiffCache + { + private readonly OpenApiDiff _openApiDiff; + private readonly OpenApiComponents _leftComponents; + private readonly OpenApiComponents _rightComponents; + + public SecuritySchemeDiff(OpenApiDiff openApiDiff) + { + _openApiDiff = openApiDiff; + _leftComponents = openApiDiff.OldSpecOpenApi?.Components; + _rightComponents = openApiDiff.NewSpecOpenApi?.Components; + } + + public ChangedSecuritySchemeBO Diff( + string leftSchemeRef, + List leftScopes, + string rightSchemeRef, + List rightScopes, + DiffContextBO context) + { + var leftSecurityScheme = _leftComponents.SecuritySchemes[leftSchemeRef]; + var rightSecurityScheme = _rightComponents.SecuritySchemes[rightSchemeRef]; + var changedSecuritySchemeOpt = + CachedDiff( + new HashSet(), + leftSecurityScheme, + rightSecurityScheme, + leftSchemeRef, + rightSchemeRef, + context); + var changedSecurityScheme = + changedSecuritySchemeOpt ?? new ChangedSecuritySchemeBO(leftSecurityScheme, rightSecurityScheme); + changedSecurityScheme = GetCopyWithoutScopes(changedSecurityScheme); + + if (changedSecurityScheme != null + && leftSecurityScheme.Type == SecuritySchemeType.OAuth2) + { + var changed = ChangedUtils.IsChanged(ListDiff.Diff( + new ChangedSecuritySchemeScopesBO(leftScopes, rightScopes) + )); + + if (changed != null) + changedSecurityScheme.ChangedScopes = changed; + } + + return ChangedUtils.IsChanged(changedSecurityScheme); + } + + protected override ChangedSecuritySchemeBO ComputeDiff( + HashSet refSet, + OpenApiSecurityScheme leftSecurityScheme, + OpenApiSecurityScheme rightSecurityScheme, + DiffContextBO context) + { + var changedSecurityScheme = + new ChangedSecuritySchemeBO(leftSecurityScheme, rightSecurityScheme) + { + Description = _openApiDiff + .MetadataDiff + .Diff(leftSecurityScheme.Description, rightSecurityScheme.Description, context) + }; + + switch (leftSecurityScheme.Type) + { + case SecuritySchemeType.ApiKey: + changedSecurityScheme.IsChangedIn = + !leftSecurityScheme.In.Equals(rightSecurityScheme.In); + break; + case SecuritySchemeType.Http: + changedSecurityScheme.IsChangedScheme = + leftSecurityScheme.Scheme != rightSecurityScheme.Scheme; + changedSecurityScheme.IsChangedBearerFormat = + leftSecurityScheme.BearerFormat != rightSecurityScheme.BearerFormat; + break; + case SecuritySchemeType.OAuth2: + changedSecurityScheme.OAuthFlows = _openApiDiff + .OAuthFlowsDiff + .Diff(leftSecurityScheme.Flows, rightSecurityScheme.Flows); + break; + case SecuritySchemeType.OpenIdConnect: + changedSecurityScheme.IsChangedOpenIdConnectUrl = + leftSecurityScheme.OpenIdConnectUrl != rightSecurityScheme.OpenIdConnectUrl; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + changedSecurityScheme.Extensions = _openApiDiff + .ExtensionsDiff + .Diff(leftSecurityScheme.Extensions, rightSecurityScheme.Extensions, context); + + return changedSecurityScheme; + } + + private static ChangedSecuritySchemeBO GetCopyWithoutScopes(ChangedSecuritySchemeBO original) + { + return new ChangedSecuritySchemeBO( + original.OldSecurityScheme, original.NewSecurityScheme) + { + IsChangedType = original.IsChangedType, + IsChangedIn = original.IsChangedIn, + IsChangedScheme = original.IsChangedScheme, + IsChangedBearerFormat = original.IsChangedBearerFormat, + Description = original.Description, + OAuthFlows = original.OAuthFlows, + IsChangedOpenIdConnectUrl = original.IsChangedOpenIdConnectUrl + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs new file mode 100644 index 000000000..f52828d65 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/ChangedElementTypeEnum.cs @@ -0,0 +1,31 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum ChangedElementTypeEnum + { + OpenApi, + Operation, + RequestBody, + Path, + Content, + Response, + Request, + Parameter, + Schema, + OneOf, + AnyOf, + AllOf, + Header, + SecurityRequirement, + SecurityScheme, + SecuritySchemeScope, + AuthFlow, + Metadata, + MediaType, + WriteOnly, + ReadOnly, + MaxLength, + Required, + Extension, + Enum + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs new file mode 100644 index 000000000..2e685e14f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/DiffResultEnum.cs @@ -0,0 +1,11 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum DiffResultEnum + { + NoChanges = 0, + Metadata = 1, + Compatible = 2, + Unknown = 3, + Incompatible = 4 + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs new file mode 100644 index 000000000..ee9ea286c --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/RefTypeEnum.cs @@ -0,0 +1,12 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum RefTypeEnum + { + RequestBodies, + Responses, + Parameters, + Schemas, + Headers, + SecuritySchemes + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs new file mode 100644 index 000000000..289011f12 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/SchemaTypeEnum.cs @@ -0,0 +1,9 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum SchemaTypeEnum + { + Schema, + ArraySchema, + ComposedSchema + } +} diff --git a/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs b/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs new file mode 100644 index 000000000..8696bbdc7 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Enums/TypeEnum.cs @@ -0,0 +1,9 @@ +namespace Microsoft.OpenApi.Diff.Enums +{ + public enum TypeEnum + { + Added, + Changed, + Removed + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs new file mode 100644 index 000000000..aa3f7fd44 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/IOpenApiPrimitiveExtensions.cs @@ -0,0 +1,17 @@ +using System.IO; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class IOpenApiPrimitiveExtensions + { + public static string GetValueString(this IOpenApiPrimitive primitive) + { + using var sb = new StringWriter(); + var writer = new OpenApiYamlWriter(sb); + primitive.Write(writer, OpenApiSpecVersion.OpenApi3_0); + return sb.ToString(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs new file mode 100644 index 000000000..f8b143505 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/ListExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class ListExtensions + { + /// + /// Determines whether the collection is null or contains no elements. + /// + /// The IEnumerable type. + /// The enumerable, which may be null or empty. + /// + /// true if the IEnumerable is null or empty; otherwise, false. + /// + public static bool IsNullOrEmpty(this IEnumerable enumerable) + { + if (enumerable == null) + { + return true; + } + /* If this is a list, use the Count property for efficiency. + * The Count property is O(1) while IEnumerable.Count() is O(N). */ + var collection = enumerable as ICollection; + if (collection != null) + { + return collection.Count < 1; + } + return !enumerable.Any(); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs new file mode 100644 index 000000000..199a99b35 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/OpenApiSchemaExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class OpenApiSchemaExtensions + { + public static SchemaTypeEnum GetSchemaType(this OpenApiSchema schema) + { + if (schema == null) + return SchemaTypeEnum.Schema; + + if (schema.Items != null) + return SchemaTypeEnum.ArraySchema; + + if (!schema.AnyOf.IsNullOrEmpty() || !schema.OneOf.IsNullOrEmpty() || !schema.AllOf.IsNullOrEmpty()) + return SchemaTypeEnum.ComposedSchema; + + return SchemaTypeEnum.Schema; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs b/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs new file mode 100644 index 000000000..542353181 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Extensions/PathExtensions.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Microsoft.OpenApi.Diff.Extensions +{ + public static class PathExtensions + { + private const string RegexPath = "\\{([^/]+)\\}"; + + public static string NormalizePath(this string path) + { + return Regex.Replace(path, RegexPath, "{}"); + } + + public static List ExtractParametersFromPath(this string path) + { + var paramsList = new List(); + var reg = new Regex(RegexPath); + var matches = reg.Matches(path); + if (!matches.IsNullOrEmpty()) + { + foreach (Match m in matches) + paramsList.Add(m.Groups[1].Value); + } + return paramsList; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs b/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs new file mode 100644 index 000000000..434faea09 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/IOpenAPICompare.cs @@ -0,0 +1,12 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace Microsoft.OpenApi.Diff +{ + public interface IOpenAPICompare + { + ChangedOpenApiBO FromLocations(string oldLocation, string newLocation, OpenApiReaderSettings settings = null); + ChangedOpenApiBO FromSpecifications(OpenApiDocument oldSpec, string oldSpecIdentifier, OpenApiDocument newSpec, string newSpecIdentifier); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj b/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj new file mode 100644 index 000000000..bc6963338 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Microsoft.OpenApi.Diff.csproj @@ -0,0 +1,44 @@ + + + + netstandard2.1 + true + http://go.microsoft.com/fwlink/?LinkID=288890 + https://github.com/Microsoft/OpenAPI.NET + https://raw.githubusercontent.com/Microsoft/OpenAPI.NET/master/LICENSE + true + Microsoft + Microsoft + Microsoft.OpenApi.Diff + Microsoft.OpenApi.Diff + 1.0.0 + Compare diffs between two OpenAPI specifications + © Microsoft Corporation. All rights reserved. + OpenAPI .NET Diff + + Microsoft.OpenApi.Diff + Microsoft.OpenApi.Diff + true + + latest + true + true + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs b/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs new file mode 100644 index 000000000..e04798a9d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/OpenApiCompare.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Compare; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace Microsoft.OpenApi.Diff +{ + public class OpenAPICompare : IOpenAPICompare + { + private readonly ILogger _logger; + private readonly IEnumerable _extensions; + + public OpenAPICompare(ILogger logger, IEnumerable extensions) + { + _logger = logger; + _extensions = extensions; + } + + public ChangedOpenApiBO FromLocations(string oldLocation, string newLocation, OpenApiReaderSettings settings = null) + { + return FromLocations(oldLocation, Path.GetFileNameWithoutExtension(oldLocation), newLocation, Path.GetFileNameWithoutExtension(newLocation), settings); + } + + public ChangedOpenApiBO FromLocations(string oldLocation, string oldIdentifier, string newLocation, string newIdentifier, OpenApiReaderSettings settings = null) + { + return FromSpecifications(ReadLocation(oldLocation, settings: settings), oldIdentifier, ReadLocation(newLocation, settings: settings), newIdentifier); + } + + public ChangedOpenApiBO FromSpecifications(OpenApiDocument oldSpec, string oldSpecIdentifier, OpenApiDocument newSpec, string newSpecIdentifier) + { + return OpenApiDiff.Compare(oldSpec, oldSpecIdentifier, newSpec, newSpecIdentifier, _extensions, _logger); + } + + private static OpenApiDocument ReadLocation(string location, List auths = null, OpenApiReaderSettings settings = null) + { + using var sr = new StreamReader(location); + + var openAPIDoc = new OpenApiStreamReader(settings).Read(sr.BaseStream, out var diagnostic); + if (!diagnostic.Errors.IsNullOrEmpty()) + throw new Exception($"Error reading file. Error: {string.Join(", ", diagnostic.Errors)}"); + + return openAPIDoc; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs b/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs new file mode 100644 index 000000000..749260a0e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/BaseRenderer.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Output +{ + public abstract class BaseRenderer + { + protected RenderViewModel GetRenderModel(ChangedOpenApiBO diff, + string reportName = "OpenAPI Compatibility Report", + string logoUrl = "", + string pageTitle = "Api Change Log", + string pageDescription = "This report was generated by Microsoft.OpenApi.Diff") + { + return new RenderViewModel + { + PageTitle = pageTitle, + Author = "Microsoft.OpenApi.Diff", + Description = pageDescription, + Name = reportName, + LogoUrl = logoUrl, + ChangeType = diff.IsChanged(), + OldSpecIdentifier = diff.OldSpecIdentifier, + NewSpecIdentifier = diff.NewSpecIdentifier, + NewEndpoints = diff.NewEndpoints + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + MissingEndpoints = diff.MissingEndpoints + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + DeprecatedEndpoints = diff.GetDeprecatedEndpoints() + .OrderBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + ChangedEndpoints = diff.ChangedOperations + .Select(x => new ChangedEndpointViewModel + { + Method = x.HttpMethod, + PathUrl = x.PathUrl, + Summary = x.Description?.Right ?? x.Summary?.Right, + ChangeType = x.IsChanged(), + ChangesByType = x.GetAllChangeInfoFlat(null) + .Where(y => !y.ChangeType.IsUnchanged()) + .Select(y => new ChangeViewModel + { + Path = y.Path.Where(z => !z.IsNullOrEmpty()).ToList(), + ChangeType = y.ChangeType, + Changes = y.Changes + .Select(z => new SingleChangeViewModel + { + ElementType = z.ElementType, + ChangeType = z.ChangeType, + FieldName = z.FieldName, + NewValue = z.NewValue, + OldValue = z.OldValue + }) + .ToList() + }) + .ToList() + }) + .OrderByDescending(x => x.ChangeType.DiffResult) + .ThenBy(x => x.PathUrl.NormalizePath()) + .ThenBy(x => x.Method) + .ToList(), + }; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs b/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs new file mode 100644 index 000000000..12dd8e26b --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/ConsoleRender.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; +using Microsoft.OpenApi.Diff.Utils; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Output +{ + public class ConsoleRender : IConsoleRender + { + private static readonly RefPointer RefPointer = new RefPointer(RefTypeEnum.Schemas); + private static ChangedOpenApiBO _diff; + + private StringBuilder _output; + + public Task Render(ChangedOpenApiBO diff) + { + _diff = diff; + _output = new StringBuilder(); + if (diff.IsUnchanged()) + { + _output.Append("No differences. Specifications are equivalents"); + } + else + { + _output + .Append(Environment.NewLine) + .Append(BigTitle("Api Change Log")) + .Append(Center(diff.NewSpecOpenApi.Info.Title)) + .Append(Environment.NewLine); + + var newEndpoints = diff.NewEndpoints; + var olNewEndpoint = ListEndpoints(newEndpoints, "What's New"); + + var missingEndpoints = diff.MissingEndpoints; + var olMissingEndpoint = ListEndpoints(missingEndpoints, "What's Deleted"); + + var deprecatedEndpoints = diff.GetDeprecatedEndpoints(); + var olDeprecatedEndpoint = ListEndpoints(deprecatedEndpoints, "What's Deprecated"); + + var changedOperations = diff.ChangedOperations; + var olChanged = OlChanged(changedOperations); + + _output + .Append(renderBody(olNewEndpoint, olMissingEndpoint, olDeprecatedEndpoint, olChanged)) + .Append(Title("Result")) + .Append( + Center( + diff.IsCompatible() + ? "API changes are backward compatible" + : "API changes broke backward compatibility")) + .Append(Environment.NewLine) + .Append(Separator('-')); + } + return Task.FromResult(_output.ToString()); + } + private static string ListEndpoints(IReadOnlyCollection endpoints, string title) + { + if (null == endpoints || endpoints.Count == 0) return ""; + var sb = new StringBuilder(); + sb.Append(Title(title)); + foreach (var endpoint in endpoints) + { + sb.Append(ItemEndpoint( + endpoint.Method.ToString(), endpoint.PathUrl, endpoint.Summary)); + } + return sb.Append(Environment.NewLine).ToString(); + } + private static string ItemEndpoint(string method, string path, string desc) + { + var sb = new StringBuilder(); + sb.Append($"- {method,6} {path}{Environment.NewLine}"); + return sb.ToString(); + } + private static string renderBody(string olNew, string olMiss, string olDeprecated, string olChanged) + { + var sb = new StringBuilder(); + sb.Append(olNew).Append(olMiss).Append(olDeprecated).Append(olChanged); + return sb.ToString(); + } + private static string BigTitle(string title) + { + const char ch = '='; + return Title(title.ToUpper(), ch); + } + private static string Title(string title, char ch = '-') + { + var little = new string(ch, 2); + var offset = little.Length * 2; + + return $"{Separator(ch)}{little}{Center(title, -offset)}{little.PadLeft(Console.WindowWidth / 2 - title.Length / 2 + little.Length)}{Environment.NewLine}{Separator(ch)}"; + } + private static StringBuilder Separator(char ch) + { + var sb = new StringBuilder(); + return sb.Append(new string(ch, Console.WindowWidth)) + .Append(Environment.NewLine); + } + private static string Center(string center, int offset = 0) + { + return string.Format("{0," + (Console.WindowWidth / 2 + center.Length / 2 + offset) + "}", center); + } + private static string OlChanged(IReadOnlyCollection operations) + { + if (null == operations || operations.Count == 0) return ""; + var sb = new StringBuilder(); + sb.Append(Title("What's Changed")); + foreach (var operation in operations) + { + var pathUrl = operation.PathUrl; + var method = operation.HttpMethod.ToString(); + var desc = operation.Summary?.Right ?? ""; + + var ul_detail = new StringBuilder(); + if (ChangedBO.Result(operation.Parameters).IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Parameter:") + .Append(Environment.NewLine) + .Append(UlParam(operation.Parameters)); + } + if (operation.ResultRequestBody().IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Request:") + .Append(Environment.NewLine) + .Append(UlContent(operation.RequestBody.Content, true)); + } + if (operation.ResultApiResponses().IsDifferent()) + { + ul_detail + .Append(new string(' ', 2)) + .Append("Return Type:") + .Append(Environment.NewLine) + .Append(UlResponse(operation.APIResponses)); + } + sb.Append(ItemEndpoint(method, pathUrl, desc)).Append(ul_detail); + } + + return sb.ToString(); + } + private static string UlParam(ChangedParametersBO changedParameters) + { + var addParameters = changedParameters.Increased; + var delParameters = changedParameters.Missing; + var changed = changedParameters.Changed; + var sb = new StringBuilder(); + foreach (var param in addParameters) + { + sb.Append(ItemParam("Add ", param)); + } + foreach (var param in changed) + { + sb.Append(LiChangedParam(param)); + } + foreach (var param in delParameters) + { + sb.Append(ItemParam("Delete ", param)); + + } + return sb.ToString(); + } + private static string UlResponse(ChangedAPIResponseBO changedApiResponse) + { + var addResponses = changedApiResponse.Increased; + var delResponses = changedApiResponse.Missing; + var changedResponses = changedApiResponse.Changed; + var sb = new StringBuilder(); + foreach (var propName in addResponses.Keys) + { + sb.Append(ItemResponse("Add ", propName)); + } + foreach (var propName in delResponses.Keys) + { + sb.Append(ItemResponse("Deleted ", propName)); + } + foreach (var propName in changedResponses.Keys) + { + sb.Append(ItemChangedResponse("Changed ", propName, changedResponses[propName])); + } + return sb.ToString(); + } + private static string ItemResponse(string title, string code) + { + var sb = new StringBuilder(); + var status = ""; + if (code != "default" && int.TryParse(code, out var statusCode)) + { + status = ((HttpStatusCode)statusCode).ToString(); + } + sb.Append(new string(' ', 4)) + .Append("- ") + .Append(title) + .Append(code) + .Append(' ') + .Append(status) + .Append(Environment.NewLine); + return sb.ToString(); + } + private static string ItemParam(string title, OpenApiParameter param) + { + var sb = new StringBuilder(""); + sb.Append(new string(' ', 4)) + .Append("- ") + .Append(title) + .Append(param.Name) + .Append(" in ") + .Append(param.In) + .Append(Environment.NewLine); + + return sb.ToString(); + } + private static string LiChangedParam(ChangedParameterBO changeParam) + { + return ItemParam(changeParam.IsDeprecated ? "Deprecated " : "Changed ", changeParam.NewParameter); + } + private static string ItemChangedResponse(string title, string contentType, ChangedResponseBO response) + { + var sb = new StringBuilder(); + sb.Append(ItemResponse(title, contentType)); + sb.Append(new string(' ', 6)).Append("Media types:").Append(Environment.NewLine); + sb.Append(UlContent(response.Content, false)); + return sb.ToString(); + } + private static string UlContent(ChangedContentBO changedContent, bool isRequest) + { + var sb = new StringBuilder(); + if (changedContent == null) + { + return sb.ToString(); + } + foreach (var propName in changedContent.Increased.Keys) + { + sb.Append(ItemContent("Added ", propName)); + } + foreach (var propName in changedContent.Missing.Keys) + { + sb.Append(ItemContent("Deleted ", propName)); + } + foreach (var propName in changedContent.Changed.Keys) + { + sb.Append(ItemContent("Changed ", propName, changedContent.Changed[propName], isRequest)); + } + return sb.ToString(); + } + private static string ItemContent(string title, string contentType) + { + var sb = new StringBuilder(); + sb.Append(new string(' ', 8)) + .Append("- ") + .Append(title) + .Append(contentType) + .Append(Environment.NewLine); + return sb.ToString(); + } + private static string ItemContent(string title, string contentType, ChangedMediaTypeBO changedMediaType, bool isRequest) + { + var sb = new StringBuilder(); + sb.Append(ItemContent(title, contentType)) + .Append(new string(' ', 10)) + .Append("Schema: ") + .Append(changedMediaType.IsCompatible() ? "Backward compatible" : "Broken compatibility") + .Append(Environment.NewLine); + if (!changedMediaType.IsCompatible()) + { + sb.Append(Incompatibilities(changedMediaType.Schema)); + } + return sb.ToString(); + } + private static string Incompatibilities(ChangedSchemaBO schema) + { + return Incompatibilities("", schema); + } + private static string Incompatibilities(string propName, ChangedSchemaBO schema) + { + var sb = new StringBuilder(); + if (schema.Items != null) + { + sb.Append(Items(propName, schema.Items)); + } + if (schema.IsCoreChanged().DiffResult == DiffResultEnum.Incompatible && schema.IsChangedType) + { + var type = schema.OldSchema.GetSchemaType() + " -> " + schema.OldSchema.GetSchemaType(); + sb.Append(Property(propName, "Changed property type", type)); + } + var prefix = propName.IsNullOrEmpty() ? "" : propName + "."; + sb.Append( + Properties(prefix, "Missing property", schema.MissingProperties, schema.Context)); + foreach (var (name, value) in schema.ChangedProperties) + { + sb.Append(Incompatibilities(prefix + name, value)); + } + return sb.ToString(); + } + private static string Items(string propName, ChangedSchemaBO schema) + { + var sb = new StringBuilder(); + sb.Append(Incompatibilities(propName + "[]", schema)); + return sb.ToString(); + } + private static string Properties(string propPrefix, string title, Dictionary properties, DiffContextBO context) + { + var sb = new StringBuilder(); + if (properties != null) + { + foreach (var (key, value) in properties) + { + sb.Append(Property(propPrefix + key, title, Resolve(value))); + } + } + return sb.ToString(); + } + private static OpenApiSchema Resolve(OpenApiSchema schema) + { + return RefPointer.ResolveRef(_diff.NewSpecOpenApi.Components, schema, schema.Reference?.ReferenceV3); + } + private static string Property(string name, string title, OpenApiSchema schema) + { + return Property(name, title, Type(schema)); + } + private static string Property(string name, string title, string type) + { + return $"{new string(' ', 10)}{title}: {name} {type}\n"; + } + private static string Type(OpenApiSchema schema) + { + var result = "object"; + if (schema.GetSchemaType() == SchemaTypeEnum.ArraySchema) + { + result = "array"; + } + else if (schema.Type != null) + { + result = schema.Type; + } + return result; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs b/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs new file mode 100644 index 000000000..40967ad9a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/HtmlRender.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Extensions; +using RazorLight; + +namespace Microsoft.OpenApi.Diff.Output.Html +{ + public class HtmlRender : BaseRenderer, IHtmlRender + { + private readonly string _title; + private readonly RazorLightEngine _engine; + + public HtmlRender() + { + _engine = new RazorLightEngineBuilder() + .UseEmbeddedResourcesProject(typeof(HtmlRender)) + .UseMemoryCachingProvider() + .Build(); + } + + public HtmlRender(string title) : this() + { + _title = title; + } + + public async Task Render(ChangedOpenApiBO diff) + { + var model = !_title.IsNullOrEmpty() ? GetRenderModel(diff, _title) : GetRenderModel(diff); + return await _engine.CompileRenderAsync("Views.Index", model); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs b/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs new file mode 100644 index 000000000..78c04b29a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/IHtmlRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output.Html +{ + public interface IHtmlRender : IRender + { + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml new file mode 100644 index 000000000..2379913d4 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangeDetail.cshtml @@ -0,0 +1,31 @@ +@using Microsoft.OpenApi.Diff.Enums +@using Microsoft.OpenApi.Diff.Extensions +@using RazorLight +@inherits TemplatePage> +@model List + + @foreach (var changeViewModel in Model) + { +
@string.Join(" -> ", changeViewModel.Path)
+
+
+ @foreach (var singleChange in changeViewModel.Changes) + { +
@singleChange.ElementType Modification
+ if (singleChange.ChangeType == TypeEnum.Changed) + { +
+ @singleChange.FieldName changed from + @(!singleChange.OldValue.IsNullOrEmpty() ? singleChange.OldValue : " ") to + @(!singleChange.NewValue.IsNullOrEmpty() ? singleChange.NewValue : " ") +
+ } + else + { +
@singleChange.ChangeType @singleChange.FieldName
+ } + } +
+
+ } + diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml new file mode 100644 index 000000000..eb2d398d0 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/ChangedOperationOverview.cshtml @@ -0,0 +1,19 @@ +@model List + + + + @foreach (var endpoint in Model) + { + + + + } + +
+ + + @endpoint.Method@endpoint.PathUrl + @endpoint.ChangeType.DiffResult + + +
diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml new file mode 100644 index 000000000..cc1f98f7a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/Index.cshtml @@ -0,0 +1,185 @@ +@using Microsoft.OpenApi.Diff.Extensions +@using RazorLight +@inherits TemplatePage +@model Microsoft.OpenApi.Diff.Output.RenderViewModel + + + + + @Model.PageTitle + + + + + + + + + + + + + +
+ +
+
+
+ +
+

Changed Endpoints Details

+
+ @foreach (var changedOperation in Model.ChangedEndpoints) + { +

+

+ @changedOperation.Method@changedOperation.PathUrl + @changedOperation.ChangeType.DiffResult +

+

+ +

@changedOperation.Summary

+ + + @if (changedOperation.ChangeType.IsIncompatible()) + { +

Breaking Changes

+
+
+ @{ await IncludeAsync("Views.ChangeDetail", changedOperation.ChangesByType.Where(x => x.ChangeType.IsIncompatible()).ToList()); } +
+ } + + @if (changedOperation.ChangesByType.Any(x => x.ChangeType.IsCompatible())) + { +

Compatible Changes

+
+
+ @{ await IncludeAsync("Views.ChangeDetail", changedOperation.ChangesByType.Where(x => x.ChangeType.IsCompatible()).ToList()); } +
+ } + } +
+
+ +
+
+
+ Created: @Model.CreatedDate + + GitHub OpenAPI Diff + +
+
+ + + diff --git a/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml b/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml new file mode 100644 index 000000000..25cb323d1 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Html/Views/OperationOverview.cshtml @@ -0,0 +1,14 @@ +@model List + + + + @foreach (var endpoint in Model) + { + + + + } + +
+ @endpoint.Method@endpoint.PathUrl +
diff --git a/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs b/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs new file mode 100644 index 000000000..39d22238a --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/IConsoleRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output +{ + public interface IConsoleRender : IRender + { + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/Output/IRender.cs b/src/Microsoft.OpenApi.Diff/Output/IRender.cs new file mode 100644 index 000000000..0fa12b76e --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/IRender.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Output +{ + public interface IRender + { + Task Render(ChangedOpenApiBO diff); + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs b/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs new file mode 100644 index 000000000..57f73c837 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Markdown/IMarkdownRender.cs @@ -0,0 +1,6 @@ +namespace Microsoft.OpenApi.Diff.Output.Markdown +{ + public interface IMarkdownRender : IRender + { + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs b/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs new file mode 100644 index 000000000..185cb3541 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/Markdown/MarkdownRender.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Diff.Extensions; + +namespace Microsoft.OpenApi.Diff.Output.Markdown +{ + public class MarkdownRender : BaseRenderer, IMarkdownRender + { + private readonly ILogger _logger; + private readonly string _title; + + public MarkdownRender(ILogger logger) + { + _logger = logger; + } + + public MarkdownRender(string title, ILogger logger) : this(logger) + { + _title = title; + } + + public Task Render(ChangedOpenApiBO diff) + { + var model = !_title.IsNullOrEmpty() ? GetRenderModel(diff, _title) : GetRenderModel(diff); + return Task.FromResult(GetIndex(model)); + } + + + private string GetIndex(RenderViewModel model) + { + return $"# {model.Name}\n" + + $"Compared Specs: **{model.OldSpecIdentifier}** - **{model.NewSpecIdentifier}**\n" + + $"Report Result: " + + $"\"{model.ChangeType.DiffResult}\"\n" + + $"## Added Endpoints\n" + + $"{GetOperationOverview(model.NewEndpoints)}\n" + + $"## Removed Endpoints\n" + + $"{GetOperationOverview(model.MissingEndpoints)}\n" + + $"## Deprecated Endpoints\n" + + $"{GetOperationOverview(model.DeprecatedEndpoints)}\n" + + $"## Changed Endpoints\n" + + $"{GetChangedOperationOverview(model.ChangedEndpoints)}"; + } + + private string GetOperationOverview(IEnumerable endpoints) + { + var returnString = string.Empty; + foreach (var endpoint in endpoints) + { + returnString += $"\"{endpoint.Method}\" **{endpoint.PathUrl}**\n"; + } + return returnString; + } + + private string GetColorForDiffResult(DiffResultEnum diffResult) + { + switch (diffResult) + { + case DiffResultEnum.NoChanges: + return "grey"; + case DiffResultEnum.Metadata: + return "blue"; + case DiffResultEnum.Compatible: + return "green"; + case DiffResultEnum.Unknown: + return "orange"; + case DiffResultEnum.Incompatible: + return "red"; + default: + throw new ArgumentOutOfRangeException(nameof(diffResult), diffResult, null); + } + } + + private string GetChangedOperationOverview(IEnumerable endpoints) + { + var returnString = string.Empty; + foreach (var endpoint in endpoints) + { + returnString += $"
\n" + + $" " + + $"\"{endpoint.Method}\" " + + $"{endpoint.PathUrl} " + + $"\"{endpoint.ChangeType.DiffResult}\"" + + $"\n" + + $" \n"; + + if (endpoint.ChangeType.IsIncompatible()) + { + returnString += $">
\n" + + $"> Breaking Changes\n" + + $"> \n" + + $"{GetChangeDetails(endpoint.ChangesByType.Where(x => x.ChangeType.IsIncompatible()))}" + + $">
\n" + + $"> \n"; + } + + if (endpoint.ChangesByType.Any(x => x.ChangeType.IsCompatible())) + { + returnString += $">
\n" + + $"> Compatible Changes\n" + + $"> \n" + + $"{GetChangeDetails(endpoint.ChangesByType.Where(x => x.ChangeType.IsCompatible()))}" + + $">
\n" + + $"> \n"; + } + + returnString += $"
\n\n"; + } + return returnString; + } + + private string GetChangeDetails(IEnumerable changes) + { + var returnString = string.Empty; + foreach (var change in changes) + { + returnString += $"> - **{string.Join(" - ", change.Path)}**\n"; + + foreach (var singleChange in change.Changes) + { + returnString += $"> - {singleChange.ElementType} Modification\n"; + + if (singleChange.ChangeType == TypeEnum.Changed) + { + returnString += $"> - `{singleChange.ElementType}` changed from " + + $"`{(!singleChange.OldValue.IsNullOrEmpty() ? singleChange.OldValue : " ")}` to " + + $"`{(!singleChange.NewValue.IsNullOrEmpty() ? singleChange.NewValue : " ")}`\n"; + } + else + { + returnString += $"> - {singleChange.ChangeType} `{singleChange.FieldName}`\n"; + } + } + } + returnString += $"> \n"; + return returnString; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs b/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs new file mode 100644 index 000000000..8490ce337 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Output/RenderViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Output +{ + public class RenderViewModel + { + public DiffResultBO ChangeType { get; set; } + public DateTime CreatedDate => DateTime.Now; + public string Author { get; set; } + public string Description { get; set; } + public string LogoUrl { get; set; } + public string Name { get; set; } + public string PageTitle { get; set; } + public IReadOnlyCollection NewEndpoints { get; set; } + public IReadOnlyCollection MissingEndpoints { get; set; } + public IReadOnlyCollection DeprecatedEndpoints { get; set; } + public IReadOnlyCollection ChangedEndpoints { get; set; } + public string OldSpecIdentifier { get; set; } + public string NewSpecIdentifier { get; set; } + } + + public class ChangedEndpointViewModel + { + public string PathUrl { get; set; } + public OperationType Method { get; set; } + public string Summary { get; set; } + public DiffResultBO ChangeType { get; set; } + public List ChangesByType { get; set; } + } + + public class ChangeViewModel + { + public List Path { get; set; } + public DiffResultBO ChangeType { get; set; } + public List Changes { get; set; } + } + + public class SingleChangeViewModel + { + public ChangedElementTypeEnum ElementType { get; set; } + public TypeEnum ChangeType { get; set; } + public string FieldName { get; set; } + public string OldValue { get; set; } + public string NewValue { get; set; } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs b/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs new file mode 100644 index 000000000..0b0363670 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/ChangedUtils.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.BusinessObjects; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public static class ChangedUtils + { + public static bool IsUnchanged(ChangedBO changed) + { + return changed == null || changed.IsUnchanged(); + } + + public static bool IsCompatible(ChangedBO changed) + { + return changed == null || changed.IsCompatible(); + } + + public static T IsChanged(T changed) + where T : ChangedBO + { + return IsUnchanged(changed) ? null : changed; + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/Copy.cs b/src/Microsoft.OpenApi.Diff/Utils/Copy.cs new file mode 100644 index 000000000..69db83c9f --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/Copy.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public static class Copy + { + public static Dictionary CopyDictionary(this Dictionary dict) + { + return dict == null ? new Dictionary() : new Dictionary(dict); + } + } +} diff --git a/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs b/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs new file mode 100644 index 000000000..8f5cf1a15 --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/EndpointUtils.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public class EndpointUtils + { + public static List ConvertToEndpoints(string pathUrl, Dictionary dict) + where T : EndpointBO, new() + { + var endpoints = new List(); + if (dict == null) return endpoints; + foreach (var (key, value) in dict) + { + var endpoint = ConvertToEndpoint(pathUrl, key, value); + endpoints.Add(endpoint); + } + return endpoints; + } + + public static T ConvertToEndpoint(string pathUrl, OperationType httpMethod, OpenApiOperation operation) + where T : EndpointBO, new() + { + var endpoint = new T + { + PathUrl = pathUrl, + Method = httpMethod, + Summary = operation.Summary, + Operation = operation + }; + return endpoint; + } + + public static List ConvertToEndpointList(Dictionary dict) + where T : EndpointBO, new() + { + var endpoints = new List(); + if (dict == null) return endpoints; + + foreach (var (key, value) in dict) + { + var operationMap = value.Operations; + foreach (var (operationType, openApiOperation) in operationMap) + { + var endpoint = new T + { + PathUrl = key, + Method = operationType, + Summary = openApiOperation.Summary, + Path = value, + Operation = openApiOperation + }; + endpoints.Add(endpoint); + } + } + return endpoints; + } + } + +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs b/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs new file mode 100644 index 000000000..5cc2d968d --- /dev/null +++ b/src/Microsoft.OpenApi.Diff/Utils/RefPointer.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.Enums; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Diff.Utils +{ + public class RefPointer + { + public const string BaseRef = "#/components/"; + private readonly RefTypeEnum _refType; + + public RefPointer(RefTypeEnum refType) + { + _refType = refType; + } + + public T ResolveRef(OpenApiComponents components, T t, string reference) + { + if (reference != null) + { + var refName = GetRefName(reference); + var maps = GetMap(components); + maps.TryGetValue(refName, out var result); + if (result == null) + { + var caseInsensitiveDictionary = new Dictionary(maps, StringComparer.OrdinalIgnoreCase); + if (caseInsensitiveDictionary.TryGetValue(refName, out var insensitiveValue)) + throw new Exception($"Reference case sensitive error. {refName} is not equal to {caseInsensitiveDictionary.First(x => x.Value.Equals(insensitiveValue)).Key}"); + + throw new AggregateException($"ref '{reference}' doesn't exist."); + } + return result; + } + return t; + } + + private Dictionary GetMap(OpenApiComponents components) + { + switch (_refType) + { + case RefTypeEnum.RequestBodies: + return (Dictionary)components.RequestBodies; + case RefTypeEnum.Responses: + return (Dictionary)components.Responses; + case RefTypeEnum.Parameters: + return (Dictionary)components.Parameters; + case RefTypeEnum.Schemas: + return (Dictionary)components.Schemas; + case RefTypeEnum.Headers: + return (Dictionary)components.Headers; + case RefTypeEnum.SecuritySchemes: + return (Dictionary)components.SecuritySchemes; + default: + throw new ArgumentOutOfRangeException("Not mapped for refType: " + _refType); + } + } + + public string GetRefName(string reference) + { + if (reference == null) + { + return null; + } + if (_refType == RefTypeEnum.SecuritySchemes) + { + return reference; + } + + var baseRef = GetBaseRefForType(_refType.GetDisplayName()); + if (!reference.StartsWith(baseRef, StringComparison.CurrentCultureIgnoreCase)) + { + throw new AggregateException("Invalid ref: " + reference); + } + return reference.Substring(baseRef.Length); + } + + private static string GetBaseRefForType(string type) + { + return $"{BaseRef}{type}/"; + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs b/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs new file mode 100644 index 000000000..e56501490 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/ITestUtils.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; + +namespace Microsoft.OpenApi.Diff.Tests +{ + public interface ITestUtils + { + void AssertOpenAPIAreEquals(string oldSpec, string newSpec); + void AssertOpenAPIChangedEndpoints(string oldSpec, string newSpec); + void AssertOpenAPIBackwardCompatible(string oldSpec, string newSpec, bool isDiff); + void AssertOpenAPIBackwardIncompatible(string oldSpec, string newSpec); + IOpenAPICompare GetOpenAPICompare(); + IEnumerable GetChangesOfType(ChangedOpenApiBO changedOpenAPI, DiffResultEnum changeType); + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj b/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj new file mode 100644 index 000000000..0dddd40b7 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Microsoft.OpenApi.Diff.Tests.csproj @@ -0,0 +1,161 @@ + + + + netcoreapp3.1 + + false + + + + + true + false + false + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml new file mode 100644 index 000000000..e5e93653c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-1.yaml @@ -0,0 +1,71 @@ +openapi: 3.0.0 +# Added by API Auto Mocking Plugin +servers: + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/anshul10s/pet-store/1.0.0 +info: + description: | + This is a sample Petstore server. You can find + out more about Swagger at + [http://swagger.io](http://swagger.io) or on + [irc.freenode.net, #swagger](http://swagger.io/irc/). + version: "1.0.0" + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities. Available, reserved, sold is must in respone. Other keys can still be there. + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/Inventory' + x-key-property : + $ref: '#/components/schemas/InvStatus' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + InvStatus: + type: string + enum: + - available + - reserved + - sold + Inventory: + type: object + properties: + id: + type: string + details: + type: count + extra_info: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml new file mode 100644 index 000000000..5b2396990 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/add-prop-2.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +# Added by API Auto Mocking Plugin +servers: + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/anshul10s/pet-store/1.0.0 +info: + description: | + This is a sample Petstore server. You can find + out more about Swagger at + [http://swagger.io](http://swagger.io) or on + [irc.freenode.net, #swagger](http://swagger.io/irc/). + version: "1.0.0" + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities. Available, reserved, sold is must in respone. Other keys can still be there. + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/Inventory' + x-key-property : + $ref: '#/components/schemas/InvStatus' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + InvStatus: + type: string + enum: + - available + - reserved + - sold + Inventory: + type: object + properties: + id: + type: string + details: + type: count \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml new file mode 100644 index 000000000..2d1cc4060 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_1.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + properties: + pet_color: + type: string + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: string + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml new file mode 100644 index 000000000..2e9702cee --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_2.yaml @@ -0,0 +1,127 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + type: string + Cat: + description: Cat class + type: object + required: + - pet_type + properties: + pet_type: + type: string + name: + type: string + pet_color: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string + pet_color: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml new file mode 100644 index 000000000..c5fd84803 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_3.yaml @@ -0,0 +1,126 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: string + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml new file mode 100644 index 000000000..38fcbb1e8 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/allOf_diff_4.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Cat' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + BasePet: + type: object + properties: + pet_color: + type: string + Pet: + allOf: + - $ref: '#/components/schemas/BasePet' + type: object + required: + - pet_type + properties: + pet_type: + nullable: false + allOf: + - type: number + Cat: + description: Cat class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + name: + type: string + Dog: + description: Dog class + allOf: + - $ref: '#/components/schemas/Pet' + type: object + properties: + bark: + type: string diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml new file mode 100644 index 000000000..4e8935e5e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_1.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Dog' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + # MyResponseType: + # oneOf: + # - $ref: '#/components/schemas/Cat' + # - $ref: '#/components/schemas/Dog' + # - $ref: '#/components/schemas/Lizard' + # discriminator: + # propertyName: pet_type + # mapping: + # dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml new file mode 100644 index 000000000..2017d5fc5 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/array_diff_2.yaml @@ -0,0 +1,132 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Cat" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + # MyResponseType: + # oneOf: + # - $ref: '#/components/schemas/Cat' + # - $ref: '#/components/schemas/Dog' + # - $ref: '#/components/schemas/Lizard' + # discriminator: + # propertyName: pet_type + # mapping: + # dog: '#/components/schemas/Dog' diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml new file mode 100644 index 000000000..39ad0ae16 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_1.yaml @@ -0,0 +1,134 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 16 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml new file mode 100644 index 000000000..621dc0625 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_2.yaml @@ -0,0 +1,152 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 24 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml new file mode 100644 index 000000000..59c400ced --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_3.yaml @@ -0,0 +1,168 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + get: + tags: + - pet + summary: Finds Pets by name + description: name can be provided for the pet + operationId: getPet + parameters: + - name: name + in: query + description: name that need to be considered for filter + required: true + schema: + type: string + responses: + "200": + description: successful operation + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 36 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml new file mode 100644 index 000000000..7da59aa85 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_4.yaml @@ -0,0 +1,157 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + deprecated: true + Dog: + type: object + properties: + bark: + type: string + test: + writeOnly: true + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml new file mode 100644 index 000000000..69f6c75ef --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/backwardCompatibility/bc_5.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + maxLength: 16 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + test: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml new file mode 100644 index 000000000..d3c5b5011 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_1.yaml @@ -0,0 +1,123 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml new file mode 100644 index 000000000..bb23d7931 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/composed_schema_2.yaml @@ -0,0 +1,129 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml new file mode 100644 index 000000000..f51b5c5ca --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_1.yaml @@ -0,0 +1,32 @@ +--- +openapi: "3.0.1" +info: + title: "Test title" + description: "This is a test metadata" + termsOfService: "http://test.com" + contact: + name: "Mark Snijder" + url: "marksnijder.nl" + email: "snijderd@gmail.com" + license: + name: "To be decided" + url: "http://test.com" + version: "version 1.0" +paths: + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: find pet by id + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: response + content: + application/json: {} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml new file mode 100644 index 000000000..53d25e979 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/content_diff_2.yaml @@ -0,0 +1,47 @@ +--- +openapi: "3.0.1" +info: + title: "Test title" + description: "This is a test metadata" + termsOfService: "http://test.com" + contact: + name: "Mark Snijder" + url: "marksnijder.nl" + email: "snijderd@gmail.com" + license: + name: "To be decided" + url: "http://test.com" + version: "version 1.0" +paths: + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: find pet by id + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: response + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: "object" + properties: + id: + type: "integer" + format: "int32" + salary: + type: "integer" + format: "int32" + name: + type: "string" \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml new file mode 100644 index 000000000..d05cc9b92 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_1.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: integer + content: + application/xml: + schema: + type: integer + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml new file mode 100644 index 000000000..2f0721bf3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/header_2.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit-New: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + content: + application/xml: + schema: + type: integer + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/Dog' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml new file mode 100644 index 000000000..bc8c3c787 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_1.yaml @@ -0,0 +1,134 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml new file mode 100644 index 000000000..9bc7cbfa4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_2.yaml @@ -0,0 +1,136 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" + Cat: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + Dog: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: "#/components/schemas/Pet" + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml new file mode 100644 index 000000000..a0626e95c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_diff_3.yaml @@ -0,0 +1,136 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/MyDog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: pet_type + mapping: + dog: '#/components/schemas/MyDog' + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Cat` + type: object + properties: + name: + type: string + MyDog: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Dog` + type: object + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + # all other properties specific to a `Lizard` + type: object + properties: + lovesRocks: + type: boolean diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml new file mode 100644 index 000000000..c0d88f136 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_1.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.1 +info: + title: oneOf test for issue 29 + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /state: + post: + operationId: update + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/A' + - $ref: '#/components/schemas/B' + discriminator: + propertyName: realtype + mapping: + a-type: '#/components/schemas/A' + b-type: '#/components/schemas/B' + required: true + responses: + '201': + description: OK +components: + schemas: + A: + type: object + properties: + realtype: + type: string + othertype: + type: string + message: + type: string + B: + type: object + properties: + realtype: + type: string + othertype: + type: string + description: + type: string + code: + type: integer + format: int32 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml new file mode 100644 index 000000000..c479c17d6 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/oneOf_discriminator-changed_2.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.1 +info: + title: oneOf test for issue 29 + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /state: + post: + operationId: update + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/A' + - $ref: '#/components/schemas/B' + discriminator: + propertyName: othertype + mapping: + a-type: '#/components/schemas/A' + b-type: '#/components/schemas/B' + required: true + responses: + '201': + description: OK +components: + schemas: + A: + type: object + properties: + realtype: + type: string + othertype: + type: string + message: + type: string + B: + type: object + properties: + realtype: + type: string + othertype: + type: string + description: + type: string + code: + type: integer + format: int32 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml new file mode 100644 index 000000000..97cc8e9b9 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml new file mode 100644 index 000000000..97cc8e9b9 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_1.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml new file mode 100644 index 000000000..a4e38e85d --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/parameters_diff_2.yaml @@ -0,0 +1,183 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 2.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + '/pet/{petId}': + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + parameters: + - name: tags + in: query + description: add new query param demo + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml new file mode 100644 index 000000000..28128a820 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_1.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml new file mode 100644 index 000000000..9eaf5a83f --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_2.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId2}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId2 + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml new file mode 100644 index 000000000..59a315cd0 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/path_3.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /pet/{petId}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input + /pet/{petId2}: + get: + tags: + - pet + summary: gets a pet by id + description: '' + operationId: updatePetWithForm + parameters: + - name: petId2 + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + responses: + '405': + description: Invalid input diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml new file mode 100644 index 000000000..a4e506262 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_1.yaml @@ -0,0 +1,670 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Muliple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value >= 1 and <= 10. Other + values will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 10 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with positive integer value. Negative + or non-integer values will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be updated + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml new file mode 100644 index 000000000..74c5525ab --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_2.yaml @@ -0,0 +1,686 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + parameters: + - name: tags + in: query + description: add new query param demo + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Muliple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image for pet + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: 'ID of pet to update, default false' + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value >= 1 and <= 10. Other + values will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 10 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with positive integer value. Negative + or non-integer values will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit-New: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: integer + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + deprecated: true + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be updated + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + userStatus: + type: integer + format: int32 + description: User Status + newUserFeild: + type: integer + format: int32 + description: a new user feild demo + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml new file mode 100644 index 000000000..eddf6ce38 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/petstore_v2_empty.yaml @@ -0,0 +1,167 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: {} +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml new file mode 100644 index 000000000..6a05ab0ad --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_1.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + message2: + type: string + details: + type: array + items: + $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml new file mode 100644 index 000000000..83d78256e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_2.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + details: + type: array + items: + $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml new file mode 100644 index 000000000..c118a2ada --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_3.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + message2: + type: string + details: + allOf: + - $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml new file mode 100644 index 000000000..5b8a7c0d4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/recursive_model_4.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: recursive test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: ping + responses: + '200': + description: OK + content: + text/plain: + schema: + $ref: '#/components/schemas/B' +components: + schemas: + B: + type: object + properties: + message: + type: string + details: + allOf: + - $ref: '#/components/schemas/B' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml new file mode 100644 index 000000000..0a31c8c81 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_1.yaml @@ -0,0 +1,162 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet1: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pet2: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet3: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + $ref: '#/components/requestBodies/Pet' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: false + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml new file mode 100644 index 000000000..4a902b711 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/request_diff_2.yaml @@ -0,0 +1,162 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 2.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet1: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet2: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + /pet3: + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + requestBody: + $ref: '#/components/requestBodies/Pet' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + newCatFeild: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml new file mode 100644 index 000000000..d08c9f1d8 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/schema_diff_cache_1.yaml @@ -0,0 +1,173 @@ +openapi: 3.0.0 +servers: + - url: "http://petstore.swagger.io/v2" +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: "http://swagger.io/terms/" + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: "http://swagger.io" + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + responses: + "405": + description: Invalid input + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/MyResponseType" + requestBody: + $ref: "#/components/requestBodies/Pet" + get: + tags: + - pet + summary: Finds Pets by name + description: name can be provided for the pet + operationId: getPet + parameters: + - name: name + in: query + description: name that need to be considered for filter + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Dog" + "400": + description: Invalid status value + security: + - petstore_auth: + - "write:pets" + - "read:pets" +externalDocs: + description: Find out more about Swagger + url: "http://swagger.io" +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + mapping: + cachorro: Dog + Cat: + type: object + properties: + name: + type: string + Dog: + type: object + properties: + bark: + type: string + Lizard: + type: object + properties: + lovesRocks: + type: boolean + + MyResponseType: + required: + - pet_type + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Lizard" + discriminator: + propertyName: pet_type + mapping: + dog: "#/components/schemas/Dog" diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml new file mode 100644 index 000000000..786df4279 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_1.yaml @@ -0,0 +1,240 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml new file mode 100644 index 000000000..2758c2d1b --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_2.yaml @@ -0,0 +1,266 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - tenant: [] + user: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth2: + - 'write:pets' + - 'read:pets' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth3: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth3: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog3' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + petstore_auth2: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml new file mode 100644 index 000000000..60b20d930 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Resources/security_diff_3.yaml @@ -0,0 +1,241 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, + #swagger](http://swagger.io/irc/). For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + - unknown: [] +paths: + '/pet/{petId}': + parameters: + - name: newHeaderParam + in: header + required: false + schema: + type: integer + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: newHeaderParam + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - petstore_auth: + - 'write:pets' + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet2: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet3: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByStatus2: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + deprecated: true + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + security: + - tenant: [] + user: [] + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + tenant: + type: apiKey + name: tenant + in: header + user: + type: apiKey + name: user + in: header + schemas: + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + type: string + name: + type: string + example: doggie + newField: + type: string + example: a field demo + description: a field demo + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs b/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs new file mode 100644 index 000000000..0bb3f2590 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/TestUtils.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Diff.BusinessObjects; +using Microsoft.OpenApi.Diff.Enums; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests +{ + public class TestUtils : ITestUtils + { + private readonly IOpenAPICompare _openAPICompare; + + public TestUtils(IOpenAPICompare openAPICompare) + { + _openAPICompare = openAPICompare; + } + + public void AssertOpenAPIAreEquals(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.Empty(changedOpenAPI.ChangedOperations); + } + + public void AssertOpenAPIChangedEndpoints(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.NotEmpty(changedOpenAPI.ChangedOperations); + } + + public void AssertOpenAPIBackwardCompatible(string oldSpec, string newSpec, bool isDiff) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.True(changedOpenAPI.IsCompatible()); + } + + public void AssertOpenAPIBackwardIncompatible(string oldSpec, string newSpec) + { + var changedOpenAPI = _openAPICompare.FromLocations(oldSpec, newSpec); + Assert.True(changedOpenAPI.IsIncompatible()); + Assert.NotEmpty(GetChangesOfType(changedOpenAPI, DiffResultEnum.Incompatible)); + } + + public IEnumerable GetChangesOfType(ChangedOpenApiBO changedOpenAPI, DiffResultEnum changeType) + { + return changedOpenAPI.GetChangedElements() + .SelectMany(x => x.Change.GetAllChangeInfoFlat(null)) + .Where(y => y.ChangeType.DiffResult == changeType) + .ToList(); + } + + public IOpenAPICompare GetOpenAPICompare() + { + return _openAPICompare; + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs new file mode 100644 index 000000000..9f529ff9e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/AddPropDiffTest.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class AddPropDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/add-prop-1.yaml"; + private const string OpenAPIDoc2 = "Resources/add-prop-2.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs new file mode 100644 index 000000000..af4f25bf3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/AllOfDiffTest.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class AllOfDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/allOf_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/allOf_diff_2.yaml"; + private const string OpenAPIDoc3 = "Resources/allOf_diff_3.yaml"; + private const string OpenAPIDoc4 = "Resources/allOf_diff_4.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffSameWithAllOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void TestDiffDifferent1() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc3); + } + + [Fact] + public void TestDiffDifferent2() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc4); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs new file mode 100644 index 000000000..7a930769e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ArrayDiffTest.cs @@ -0,0 +1,23 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ArrayDiffTest : BaseTest + { + private const string OpenAPIDoc31 = "Resources/array_diff_1.yaml"; + private const string OpenAPIDoc32 = "Resources/array_diff_2.yaml"; + + [Fact] + public void TestArrayDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc31, OpenAPIDoc32); + } + + [Fact] + public void TestArrayDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc31, OpenAPIDoc31); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs new file mode 100644 index 000000000..4d62b92f3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/BackwardCompatibilityTest.cs @@ -0,0 +1,56 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class BackwardCompatibilityTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/backwardCompatibility/bc_1.yaml"; + private const string OpenAPIDoc2 = "Resources/backwardCompatibility/bc_2.yaml"; + private const string OpenAPIDoc3 = "Resources/backwardCompatibility/bc_3.yaml"; + private const string OpenAPIDoc4 = "Resources/backwardCompatibility/bc_4.yaml"; + private const string OpenAPIDoc5 = "Resources/backwardCompatibility/bc_5.yaml"; + + [Fact] + public void TestNoChange() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc1, false); + } + + [Fact] + public void TestAPIAdded() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc2, true); + } + + [Fact] + public void TestAPIMissing() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc2, OpenAPIDoc1); + } + + [Fact] + public void TestAPIChangedOperationAdded() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc2, OpenAPIDoc3, true); + } + + [Fact] + public void TestAPIChangedOperationMissing() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc3, OpenAPIDoc2); + } + + [Fact] + public void TestAPIOperationChanged() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc2, OpenAPIDoc4, true); + } + + [Fact] + public void TestAPIReadWriteOnlyPropertiesChanged() + { + TestUtils.AssertOpenAPIBackwardCompatible(OpenAPIDoc1, OpenAPIDoc5, true); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs new file mode 100644 index 000000000..f464b724e --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ContentDiffTest.cs @@ -0,0 +1,32 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ContentDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/content_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/content_diff_2.yaml"; + + [Fact] + public void TestContentDiffWithOneEmptyMediaType() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + Assert.True(changedOpenAPI.IsIncompatible()); + } + + [Fact] + public void TestContentDiffWithEmptyMediaTypes() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc1); + Assert.True(changedOpenAPI.IsUnchanged()); + } + + [Fact] + public void TestSameContentDiff() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc2, OpenAPIDoc2); + Assert.True(changedOpenAPI.IsUnchanged()); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs new file mode 100644 index 000000000..9268bc6ff --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/OneOfDiffTest.cs @@ -0,0 +1,47 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class OneOfDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/oneOf_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/oneOf_diff_2.yaml"; + private const string OpenAPIDoc3 = "Resources/oneOf_diff_3.yaml"; + private const string OpenAPIDoc4 = "Resources/composed_schema_1.yaml"; + private const string OpenAPIDoc5 = "Resources/composed_schema_2.yaml"; + private const string OpenAPIDoc6 = "Resources/oneOf_discriminator-changed_1.yaml"; + private const string OpenAPIDoc7 = "Resources/oneOf_discriminator-changed_2.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffDifferentMapping() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void testDiffSameWithOneOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc2, OpenAPIDoc3); + } + + [Fact] + public void TestComposedSchema() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc4, OpenAPIDoc5); + } + + [Fact] + public void TestOneOfDiscriminatorChanged() + { + // The oneOf 'discriminator' changed: 'realtype' -> 'othertype': + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc6, OpenAPIDoc7); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs new file mode 100644 index 000000000..32f72121c --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/OpenApiDiffTest.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.OpenApi.Diff.Output.Html; +using Microsoft.OpenApi.Diff.Output.Markdown; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class OpenAPIDiffTest : BaseTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public OpenAPIDiffTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + private const string OpenAPIDoc1 = "Resources/petstore_v2_1.yaml"; + private const string OpenAPIDoc2 = "Resources/petstore_v2_2.yaml"; + private const string OpenAPIEmptyDoc = "Resources/petstore_v2_empty.yaml"; + + [Fact] + public void TestEqual() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc2, OpenAPIDoc2); + } + + [Fact] + public async void TestNewAPI() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIEmptyDoc, OpenAPIDoc2); + var newEndpoints = changedOpenAPI.NewEndpoints; + var missingEndpoints = changedOpenAPI.MissingEndpoints; + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + + try + { + File.WriteAllText("testNewAPI.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.NotEmpty(newEndpoints); + Assert.Empty(missingEndpoints); + Assert.Empty(changedEndPoints); + } + + [Fact] + public async Task TestDeprecatedAPI() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIEmptyDoc); + var newEndpoints = changedOpenAPI.NewEndpoints; + var missingEndpoints = changedOpenAPI.MissingEndpoints; + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + + try + { + File.WriteAllText("testDeprecatedAPI.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.Empty(newEndpoints); + Assert.NotEmpty(missingEndpoints); + Assert.Empty(changedEndPoints); + } + + [Fact] + public async Task TestDiff() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + var changedEndPoints = changedOpenAPI.ChangedOperations; + var html = await new HtmlRender().Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.html", html); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + Assert.NotEmpty(changedEndPoints); + } + + [Fact] + public async Task TestDiffAndMarkdown() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + var logger = _testOutputHelper.BuildLoggerFor(); + var render = await new MarkdownRender(logger).Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.md", render); + + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + } + + [Fact] + public async Task TestDiffAndHtml() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenAPIDoc1, OpenAPIDoc2); + //var incompatibleChanges = TestUtils.GetChangesOfType(changedOpenAPI, DiffResultEnum.Incompatible); + var render = await new HtmlRender().Render(changedOpenAPI); + try + { + File.WriteAllText("testDiff.html", render); + + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs new file mode 100644 index 000000000..d48686fb3 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ParameterDiffTest.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ParameterDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/parameters_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/parameters_diff_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs new file mode 100644 index 000000000..2fcaf7537 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/PathDiffTest.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class PathDiffTest : BaseTest + { + private const string OpenAPIPath1 = "Resources/path_1.yaml"; + private const string OpenAPIPath2 = "Resources/path_2.yaml"; + private const string OpenAPIPath3 = "Resources/path_3.yaml"; + + [Fact] + public void TestEqual() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIPath1, OpenAPIPath2); + } + + [Fact] + public void TestMultiplePathWithSameSignature() + { + Assert.Throws(() => TestUtils.AssertOpenAPIAreEquals(OpenAPIPath3, OpenAPIPath3)); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs new file mode 100644 index 000000000..25d0595e0 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/RecursiveSchemaTest.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class RecursiveSchemaTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/recursive_model_1.yaml"; + private const string OpenAPIDoc2 = "Resources/recursive_model_2.yaml"; + private const string OpenAPIDoc3 = "Resources/recursive_model_3.yaml"; + private const string OpenAPIDoc4 = "Resources/recursive_model_4.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + + [Fact] + public void TestDiffSameWithAllOf() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc3, OpenAPIDoc3); + } + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc1, OpenAPIDoc2); + } + + [Fact] + public void TestDiffDifferentWithAllOf() + { + TestUtils.AssertOpenAPIBackwardIncompatible(OpenAPIDoc3, OpenAPIDoc4); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs new file mode 100644 index 000000000..75becbfd1 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ReferenceDiffCacheTest.cs @@ -0,0 +1,16 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ReferenceDiffCacheTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/schema_diff_cache_1.yaml"; + + [Fact] + public void TestDiffSame() + { + TestUtils.AssertOpenAPIAreEquals(OpenAPIDoc1, OpenAPIDoc1); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs new file mode 100644 index 000000000..c41815cbd --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/RequestDiffTest.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class RequestDiffTest : BaseTest + { + private const string OpenAPIDoc1 = "Resources/request_diff_1.yaml"; + private const string OpenAPIDoc2 = "Resources/request_diff_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + TestUtils.AssertOpenAPIChangedEndpoints(OpenAPIDoc1, OpenAPIDoc2); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs new file mode 100644 index 000000000..6bf8fa9e4 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/ResponseHeaderDiffTest.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Microsoft.OpenApi.Diff.Tests._Base; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class ResponseHeaderDiffTest : BaseTest + { + private const string OpenapiDoc1 = "Resources/header_1.yaml"; + private const string OpenapiDoc2 = "Resources/header_2.yaml"; + + [Fact] + public void TestDiffDifferent() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc1, OpenapiDoc2); + + Assert.Empty(changedOpenAPI.NewEndpoints); + Assert.Empty(changedOpenAPI.MissingEndpoints); + Assert.NotEmpty(changedOpenAPI.ChangedOperations); + + var changedResponses = changedOpenAPI.ChangedOperations.FirstOrDefault()?.APIResponses.Changed; + + Assert.NotNull(changedResponses); + Assert.NotEmpty(changedResponses); + Assert.True(changedResponses.ContainsKey("200")); + + var changedHeaders = changedResponses["200"].Headers; + Assert.True(changedHeaders.IsDifferent()); + Assert.Single(changedHeaders.Changed); + Assert.Single(changedHeaders.Increased); + Assert.Single(changedHeaders.Missing); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs b/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs new file mode 100644 index 000000000..b6b127090 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/Tests/SecurityDiffTest.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using Microsoft.OpenApi.Diff.Tests._Base; +using Microsoft.OpenApi.Readers; +using Xunit; + +namespace Microsoft.OpenApi.Diff.Tests.Tests +{ + public class SecurityDiffTest : BaseTest + { + private const string OpenapiDoc1 = "Resources/security_diff_1.yaml"; + private const string OpenapiDoc2 = "Resources/security_diff_2.yaml"; + private const string OpenapiDoc3 = "Resources/security_diff_3.yaml"; + + [Fact] + public void TestDiffDifferent() + { + var changedOpenAPI = TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc1, OpenapiDoc2); + Assert.Equal(3, changedOpenAPI.ChangedOperations.Count); + + var changedOperation1 = changedOpenAPI + .ChangedOperations + .FirstOrDefault(x => x.PathUrl.Equals("/pet/{petId}")); + Assert.NotNull(changedOperation1); + Assert.False(changedOperation1.IsCompatible()); + + var changedSecurityRequirements1 = changedOperation1.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements1); + Assert.False(changedSecurityRequirements1.IsCompatible()); + Assert.Single(changedSecurityRequirements1.Increased); + Assert.Single(changedSecurityRequirements1.Changed); + + var changedSecurityRequirement1 = changedSecurityRequirements1.Changed.FirstOrDefault(); + + Assert.NotNull(changedSecurityRequirement1); + Assert.Single(changedSecurityRequirement1.Changed); + + var changedScopes1 = + changedSecurityRequirement1.Changed.First().ChangedScopes; + Assert.NotNull(changedScopes1); + Assert.Single(changedScopes1.Increased); + Assert.Equal("read:pets", changedScopes1.Increased.First()); + + var changedOperation2 = + changedOpenAPI + .ChangedOperations.FirstOrDefault(x => x.PathUrl == "/pet3"); + Assert.NotNull(changedOperation2); + Assert.False(changedOperation2.IsCompatible()); + + var changedSecurityRequirements2 = + changedOperation2.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements2); + Assert.False(changedSecurityRequirements2.IsCompatible()); + Assert.Single(changedSecurityRequirements2.Changed); + + var changedSecurityRequirement2 = + changedSecurityRequirements2.Changed.First(); + Assert.Single(changedSecurityRequirement2.Changed); + + var changedImplicitOAuthFlow2 = + changedSecurityRequirement2.Changed.First().OAuthFlows.ImplicitOAuthFlow; + Assert.NotNull(changedImplicitOAuthFlow2); + Assert.True(changedImplicitOAuthFlow2.ChangedAuthorizationUrl); + + var changedOperation3 = + changedOpenAPI + .ChangedOperations + .FirstOrDefault(x => x.PathUrl == "/pet/findByStatus2"); + Assert.NotNull(changedOperation3); + Assert.True(changedOperation3.IsCompatible()); + + var changedSecurityRequirements3 = + changedOperation3.SecurityRequirements; + Assert.NotNull(changedSecurityRequirements3); + Assert.Single(changedSecurityRequirements3.Increased); + + var securityRequirement3 = changedSecurityRequirements3.Increased.First(); + Assert.Single(securityRequirement3); + Assert.All(securityRequirement3.Keys, x => Assert.Equal("petstore_auth", x.Reference.ReferenceV3)); + Assert.Equal(2, securityRequirement3.First().Value.Count); + } + + [Fact] + public void TestWithUnknownSecurityScheme() + { + var settings = new OpenApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences + }; + Assert.Throws(() => TestUtils.GetOpenAPICompare().FromLocations(OpenapiDoc3, OpenapiDoc3, settings)); + } + } +} diff --git a/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs b/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs new file mode 100644 index 000000000..088822333 --- /dev/null +++ b/test/Microsoft.OpenApi.Diff.Tests/_Base/BaseTest.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Diff.Compare; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Diff.Tests._Base +{ + public class BaseTest + { + public readonly ITestUtils TestUtils; + public readonly ITestOutputHelper OutputHelper; + + public BaseTest() + { + var services = new ServiceCollection(); + services.AddTransient(); + services.AddTransient(); + services.AddLogging(); + services.RegisterAll(new[] {GetType().Assembly}, ServiceLifetime.Transient); + + var serviceProvider = services.BuildServiceProvider(); + + TestUtils = serviceProvider.GetService(); + OutputHelper = serviceProvider.GetService(); + } + + + } + + public static class ServiceCollectionExtension { + public static void RegisterAll(this IServiceCollection serviceCollection, Assembly[] assemblies, ServiceLifetime lifetime) + { + var typesFromAssemblies = assemblies + .SelectMany(a => a.DefinedTypes.Where(x => x.GetInterfaces().Contains(typeof(T)))); + + foreach (var type in typesFromAssemblies) + serviceCollection.Add(new ServiceDescriptor(typeof(T), type, lifetime)); + } + } +}