diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..892f86c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{cs,vb}] + +# IDE0009: Member access should be qualified. +dotnet_diagnostic.IDE0009.severity = none diff --git a/Microsoft.OpenApi.OData.sln b/Microsoft.OpenApi.OData.sln index a883521..7c8c29f 100644 --- a/Microsoft.OpenApi.OData.sln +++ b/Microsoft.OpenApi.OData.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.OData.Reader", "src\Microsoft.OpenApi.OData.Reader\Microsoft.OpenApi.OData.Reader.csproj", "{FF3ACD93-19E0-486C-9C0F-FA1C2E7FC8C2}" EndProject @@ -11,6 +11,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OoasUtil", "src\OoasUtil\Oo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OoasGui", "src\OoasGui\OoasGui.csproj", "{79B190E8-EDB0-4C03-8FD8-EB48E4807CFB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99C5C9A7-63FD-4E78-96E8-69C402868C3E}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/IODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/IODataPathProvider.cs index dd06f83..0b9b87b 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/IODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/IODataPathProvider.cs @@ -24,7 +24,8 @@ namespace Microsoft.OpenApi.OData.Edm /// Generate the list of based on the given . /// /// The Edm model. + /// The conversion settings. /// The collection of built . - IEnumerable GetPaths(IEdmModel model); + IEnumerable GetPaths(IEdmModel model, OpenApiConvertSettings settings); } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataContext.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataContext.cs index cba4b32..2454432 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataContext.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataContext.cs @@ -158,7 +158,7 @@ namespace Microsoft.OpenApi.OData.Edm /// All acceptable OData path. private IEnumerable LoadAllODataPaths() { - IEnumerable allPaths = _pathProvider.GetPaths(Model); + IEnumerable allPaths = _pathProvider.GetPaths(Model, Settings); foreach (var path in allPaths) { if ((path.Kind == ODataPathKind.Operation && !Settings.EnableOperationPath) || diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index 30dea63..44d735e 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -3,10 +3,12 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Vocabularies; namespace Microsoft.OpenApi.OData.Edm { @@ -38,8 +40,9 @@ namespace Microsoft.OpenApi.OData.Edm /// Generate the list of based on the given . /// /// The Edm model. + /// The conversion settings. /// The collection of built . - public virtual IEnumerable GetPaths(IEdmModel model) + public virtual IEnumerable GetPaths(IEdmModel model, OpenApiConvertSettings settings) { if (model == null || model.EntityContainer == null) { @@ -67,7 +70,7 @@ namespace Microsoft.OpenApi.OData.Edm } // bound operations - RetrieveBoundOperationPaths(); + RetrieveBoundOperationPaths(settings); // unbound operations foreach (IEdmOperationImport import in _model.EntityContainer.OperationImports()) @@ -338,7 +341,7 @@ namespace Microsoft.OpenApi.OData.Edm /// /// Retrieve all bounding . /// - private void RetrieveBoundOperationPaths() + private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings) { foreach (var edmOperation in _model.GetAllElements().OfType().Where(e => e.IsBound)) { @@ -396,10 +399,17 @@ namespace Microsoft.OpenApi.OData.Edm } // 3. Search for derived - if (AppendBoundOperationOnDerived(edmOperation, isCollection, bindingEntityType)) + if (AppendBoundOperationOnDerived(edmOperation, isCollection, bindingEntityType, convertSettings)) { continue; } + + // 4. Search for derived generated navigation property + if (AppendBoundOperationOnDerivedNavigationPropertyPath(edmOperation, isCollection, bindingEntityType, convertSettings)) + { + continue; + } + } } } @@ -477,7 +487,11 @@ namespace Microsoft.OpenApi.OData.Edm return found; } - private bool AppendBoundOperationOnDerived(IEdmOperation edmOperation, bool isCollection, IEdmEntityType bindingEntityType) + private bool AppendBoundOperationOnDerived( + IEdmOperation edmOperation, + bool isCollection, + IEdmEntityType bindingEntityType, + OpenApiConvertSettings convertSettings) { bool found = false; @@ -488,6 +502,14 @@ namespace Microsoft.OpenApi.OData.Edm { foreach (var ns in baseNavigationSource) { + if (HasUnsatisfiedDerivedTypeConstraint( + ns as IEdmVocabularyAnnotatable, + baseType, + convertSettings)) + { + continue; + } + if (isCollection) { if (ns is IEdmEntitySet) @@ -523,5 +545,84 @@ namespace Microsoft.OpenApi.OData.Edm return found; } + private bool HasUnsatisfiedDerivedTypeConstraint( + IEdmVocabularyAnnotatable annotatable, + IEdmEntityType baseType, + OpenApiConvertSettings convertSettings) + { + return convertSettings.RequireDerivedTypesConstraintForBoundOperations && + !(_model.GetCollection(annotatable, "Org.OData.Validation.V1.DerivedTypeConstraint") ?? Enumerable.Empty()) + .Any(c => c.Equals(baseType.FullName(), StringComparison.OrdinalIgnoreCase)); + } + + private bool AppendBoundOperationOnDerivedNavigationPropertyPath( + IEdmOperation edmOperation, + bool isCollection, + IEdmEntityType bindingEntityType, + OpenApiConvertSettings convertSettings) + { + bool found = false; + bool isEscapedFunction = _model.IsUrlEscapeFunction(edmOperation); + + foreach (var baseType in bindingEntityType.FindAllBaseTypes()) + { + if (_allNavigationPropertyPaths.TryGetValue(baseType, out IList paths)) + { + foreach (var path in paths) + { + if (path.Kind == ODataPathKind.Ref) + { + continue; + } + + var npSegment = path.Segments.Last(s => s is ODataNavigationPropertySegment) + as ODataNavigationPropertySegment; + if (npSegment == null) + { + continue; + } + + bool isLastKeySegment = path.LastSegment is ODataKeySegment; + + if (isCollection) + { + if (isLastKeySegment) + { + continue; + } + + if (npSegment.NavigationProperty.TargetMultiplicity() != EdmMultiplicity.Many) + { + continue; + } + } + else + { + if (!isLastKeySegment && npSegment.NavigationProperty.TargetMultiplicity() == + EdmMultiplicity.Many) + { + continue; + } + } + + if (HasUnsatisfiedDerivedTypeConstraint( + npSegment.NavigationProperty as IEdmVocabularyAnnotatable, + baseType, + convertSettings)) + { + continue; + } + + ODataPath newPath = path.Clone(); + newPath.Push(new ODataTypeCastSegment(bindingEntityType)); + newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction)); + AppendPath(newPath); + found = true; + } + } + } + + return found; + } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs index 74df7cf..a252878 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs @@ -161,6 +161,13 @@ namespace Microsoft.OpenApi.OData /// public bool ShowSchemaExamples { get; set; } = false; + /// + /// Gets/Sets a value indicating whether or not to require the + /// Validation.DerivedTypeConstraint to be applied to NavigationSources + /// to bind operations of derived types to them. + /// + public bool RequireDerivedTypesConstraintForBoundOperations { get; set; } = false; + /// /// Gets/sets a value indicating whether or not to show the root path of the described API. /// @@ -197,6 +204,7 @@ namespace Microsoft.OpenApi.OData EnableDerivedTypesReferencesForRequestBody = this.EnableDerivedTypesReferencesForRequestBody, RoutePathPrefixProvider = this.RoutePathPrefixProvider, ShowLinks = this.ShowLinks, + RequireDerivedTypesConstraintForBoundOperations = this.RequireDerivedTypesConstraintForBoundOperations, ShowSchemaExamples = this.ShowSchemaExamples, ShowRootPath = this.ShowRootPath, PathProvider = this.PathProvider diff --git a/src/OoasGui/OoasGui.csproj b/src/OoasGui/OoasGui.csproj index b1db143..cefa390 100644 --- a/src/OoasGui/OoasGui.csproj +++ b/src/OoasGui/OoasGui.csproj @@ -71,6 +71,9 @@ True Resources.resx + + .editorconfig + SettingsSingleFileGenerator diff --git a/src/OoasUtil/ComLineProcesser.cs b/src/OoasUtil/ComLineProcesser.cs index 02966f3..bea0f1d 100644 --- a/src/OoasUtil/ComLineProcesser.cs +++ b/src/OoasUtil/ComLineProcesser.cs @@ -74,22 +74,29 @@ namespace OoasUtil /// Set the output to expect all derived types in request bodies. /// public bool? DerivedTypesReferencesForRequestBody { get; private set; } - + /// /// Set the output to expose pagination for collections. /// public bool? EnablePagination { get; private set; } /// - /// tSet the output to use unqualified calls for bound operations. + /// Set the output to use unqualified calls for bound operations. /// public bool? EnableUnqualifiedCall { get; private set; } /// - /// tDisable examples in the schema. + /// Disable examples in the schema. /// public bool? DisableSchemaExamples { get; private set; } + /// + /// Gets/Sets a value indicating whether or not to require the + /// Validation.DerivedTypeConstraint to be applied to NavigationSources + /// to bind operations of derived types to them. + /// + public bool? RequireDerivedTypesConstraint { get; private set; } + /// /// Process the arguments. /// @@ -186,6 +193,14 @@ namespace OoasUtil } break; + case "--requireDerivedTypesConstraint": + case "-rdt": + if (!ProcessRequireDerivedTypesConstraint(true)) + { + return false; + } + break; + case "--enablepagination": case "-p": if (!ProcessEnablePagination(true)) @@ -248,6 +263,11 @@ namespace OoasUtil DerivedTypesReferencesForRequestBody = false; } + if (RequireDerivedTypesConstraint == null) + { + RequireDerivedTypesConstraint = false; + } + if (EnablePagination == null) { EnablePagination = false; @@ -345,6 +365,19 @@ namespace OoasUtil return true; } + private bool ProcessRequireDerivedTypesConstraint(bool requireDerivedTypesConstraint) + { + if (RequireDerivedTypesConstraint != null) + { + Console.WriteLine("[Error:] Multiple [--requireDerivedTypesConstraint|-rdt] are not allowed.\n"); + PrintUsage(); + return false; + } + + RequireDerivedTypesConstraint = requireDerivedTypesConstraint; + return true; + } + private bool ProcessEnablePagination(bool enablePagination) { if (EnablePagination != null) @@ -445,6 +478,7 @@ namespace OoasUtil sb.Append(" --keyassegment|-k\t\t\tSet the output to use key-as-segment style URLs.\n"); sb.Append(" --derivedtypesreferencesforresponses|-drs\t\t\tSet the output to produce all derived types in responses.\n"); sb.Append(" --derivedtypesreferencesforrequestbody|-drq\t\t\tSet the output to expect all derived types in request bodies.\n"); + sb.Append(" --requireDerivedTypesConstraint|-rdt\t\t\tSet the output to require derived type constraint to bind Operations.\n"); sb.Append(" --enablepagination|-p\t\t\tSet the output to expose pagination for collections.\n"); sb.Append(" --enableunqualifiedcall|-u\t\t\tSet the output to use unqualified calls for bound operations.\n"); sb.Append(" --disableschemaexamples|-x\t\t\tDisable examples in the schema.\n"); diff --git a/src/OoasUtil/Program.cs b/src/OoasUtil/Program.cs index eae7817..b5ba308 100644 --- a/src/OoasUtil/Program.cs +++ b/src/OoasUtil/Program.cs @@ -37,6 +37,7 @@ namespace OoasUtil EnableKeyAsSegment = processer.KeyAsSegment, EnableDerivedTypesReferencesForResponses = processer.DerivedTypesReferencesForResponses.Value, EnableDerivedTypesReferencesForRequestBody = processer.DerivedTypesReferencesForRequestBody.Value, + RequireDerivedTypesConstraintForBoundOperations = processer.RequireDerivedTypesConstraint.Value, EnablePagination = processer.EnablePagination.Value, EnableUnqualifiedCall = processer.EnableUnqualifiedCall.Value, ShowSchemaExamples = !processer.DisableSchemaExamples.Value, diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs index e189e94..a698a1d 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs @@ -4,12 +4,14 @@ // ------------------------------------------------------------ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using System.Xml.Linq; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Csdl; +using Microsoft.OData.Edm.Validation; using Microsoft.OpenApi.OData.Tests; using Xunit; @@ -23,9 +25,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests // Arrange IEdmModel model = new EdmModel(); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -37,25 +40,174 @@ namespace Microsoft.OpenApi.OData.Edm.Tests { // Arrange IEdmModel model = EdmModelHelper.GraphBetaModel; + var settings = new OpenApiConvertSettings(); ODataPathProvider provider = new ODataPathProvider(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(4887, paths.Count()); + } + + [Fact] + public void GetPathsForGraphBetaModelWithDerivedTypesConstraintReturnsAllPaths() + { + // Arrange + IEdmModel model = EdmModelHelper.GraphBetaModel; + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true + }; + + // Act + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); Assert.Equal(4544, paths.Count()); } + [Fact] + public void GetPathsForInheritanceModelWithoutDerivedTypesConstraintReturnsMore() + { + // Arrange + IEdmModel model = GetInheritanceModel(string.Empty); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(3, paths.Count()); + } + + [Fact] + public void GetPathsForInheritanceModelWithDerivedTypesConstraintNoAnnotationReturnsFewer() + { + // Arrange + IEdmModel model = GetInheritanceModel(string.Empty); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true + }; + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(2, paths.Count()); + } + + [Fact] + public void GetPathsForInheritanceModelWithDerivedTypesConstraintWithAnnotationReturnsMore() + { + // Arrange + IEdmModel model = GetInheritanceModel(@" + + + NS.Customer + NS.NiceCustomer + +"); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true + }; + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.Equal(3, paths.Count()); + } + +#if DEBUG + // Super useful for debugging tests. + private string ListToString(IEnumerable paths) + { + return string.Join(Environment.NewLine, + paths.Select(p => string.Join("/", p.Segments.Select(s => s.Identifier)))); + } +#endif + + [Fact] + public void GetPathsForNavPropModelWithoutDerivedTypesConstraintReturnsMore() + { + // Arrange + IEdmModel model = GetNavPropModel(string.Empty); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(4, paths.Count()); + } + + [Fact] + public void GetPathsForNavPropModelWithDerivedTypesConstraintNoAnnotationReturnsFewer() + { + // Arrange + IEdmModel model = GetNavPropModel(string.Empty); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true + }; + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(3, paths.Count()); + } + + [Fact] + public void GetPathsForNavPropModelWithDerivedTypesConstraintWithAnnotationReturnsMore() + { + // Arrange + IEdmModel model = GetNavPropModel(@" + + + NS.Customer + NS.NiceCustomer + +"); + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true + }; + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(4, paths.Count()); + } + [Fact] public void GetPathsForSingleEntitySetWorks() { // Arrange IEdmModel model = GetEdmModel("", ""); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -69,9 +221,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests // Arrange IEdmModel model = GetEdmModel("", @""); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -90,9 +243,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests "; IEdmModel model = GetEdmModel(boundFunction, ""); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -111,9 +265,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests "; IEdmModel model = GetEdmModel(boundAction, ""); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -137,9 +292,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests IEdmModel model = GetEdmModel(boundAction, unbounds); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -165,9 +321,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests IEdmModel model = GetEdmModel(entityType, entitySet); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -196,9 +353,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests string entitySet = @""; IEdmModel model = GetEdmModel(entityType, entitySet); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model, settings); // Assert Assert.NotNull(paths); @@ -220,9 +378,10 @@ namespace Microsoft.OpenApi.OData.Edm.Tests // Arrange IEdmModel model = GetEdmModel(hasStream, streamPropName); ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings(); // Act - var paths = provider.GetPaths(model); + var paths = provider.GetPaths(model,settings); // Assert Assert.NotNull(paths); @@ -250,7 +409,7 @@ namespace Microsoft.OpenApi.OData.Edm.Tests private static IEdmModel GetEdmModel(string schemaElement, string containerElement) { - string template = @" + string template = $@" @@ -258,15 +417,76 @@ namespace Microsoft.OpenApi.OData.Edm.Tests - {0} + {schemaElement} - {1} + {containerElement} "; - string schema = string.Format(template, schemaElement, containerElement); - bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out _); - Assert.True(parsed); + return GetEdmModel(template); + } + + private static IEdmModel GetInheritanceModel(string annotation) + { + string template = $@" + + + + + + + + + + + + + + + + {annotation} + + +"; + return GetEdmModel(template); + } + + private static IEdmModel GetNavPropModel(string annotation) + { + string template = $@" + + + + + + + + {annotation} + + + + + + + + + + + + + + + + + +"; + return GetEdmModel(template); + } + + private static IEdmModel GetEdmModel(string schema) + { + bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out IEnumerable errors); + Assert.True(parsed, $"Parse failure. {string.Join(Environment.NewLine, errors.Select(e => e.ToString()))}"); return parsedModel; }