diff --git a/CHANGELOG.md b/CHANGELOG.md index 9473854a1a..7765008e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Put opening brace after property definition on new line, if property has getter and setter [#4625](https://github.com/microsoft/kiota/issues/4625) - Put spaces correctly around dictionary entries [#4625](https://github.com/microsoft/kiota/issues/4625) - Remove trailing space after class definition [#4625](https://github.com/microsoft/kiota/issues/4625) +- Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) +- Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) +- Fixed a bug where some allOf scenarios would be missing properties if type object wasn't set on the schema. [#4074](https://github.com/microsoft/kiota/issues/4074) +- Fixed a bug where schema with multiple allOf entries would incorrectly get merged to inherit from the first entry [#4428] (https://github.com/microsoft/kiota/issues/4428) - Fixes constructor generation for nullable properties that are initialized as null in C#,Java and PHP ## [1.14.0] - 2024-05-02 diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 4c705bdeeb..17a67c842c 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -10,41 +10,37 @@ public static class OpenApiSchemaExtensions { private static readonly Func> classNamesFlattener = x => (x.AnyOf ?? Enumerable.Empty()).Union(x.AllOf).Union(x.OneOf).ToList(); - public static IEnumerable GetSchemaNames(this OpenApiSchema schema) + public static IEnumerable GetSchemaNames(this OpenApiSchema schema, bool directOnly = false) { if (schema == null) - return Enumerable.Empty(); - if (schema.Items != null) + return []; + if (!directOnly && schema.Items != null) return schema.Items.GetSchemaNames(); if (!string.IsNullOrEmpty(schema.Reference?.Id)) - return new[] { schema.Reference.Id.Split('/')[^1].Split('.')[^1] }; - if (schema.AnyOf.Any()) + return [schema.Reference.Id.Split('/')[^1].Split('.')[^1]]; + if (!directOnly && schema.AnyOf.Any()) return schema.AnyOf.FlattenIfRequired(classNamesFlattener); - if (schema.AllOf.Any()) + if (!directOnly && schema.AllOf.Any()) return schema.AllOf.FlattenIfRequired(classNamesFlattener); - if (schema.OneOf.Any()) + if (!directOnly && schema.OneOf.Any()) return schema.OneOf.FlattenIfRequired(classNamesFlattener); - if (!string.IsNullOrEmpty(schema.Title)) - return new[] { schema.Title }; - if (!string.IsNullOrEmpty(schema.Xml?.Name)) - return new[] { schema.Xml.Name }; - return Enumerable.Empty(); + return []; } internal static IEnumerable FlattenSchemaIfRequired(this IList schemas, Func> subsequentGetter) { - if (schemas is null) return Enumerable.Empty(); - return schemas.Count == 1 ? + if (schemas is null) return []; + return schemas.Count == 1 && !schemas[0].HasAnyProperty() ? schemas.FlattenEmptyEntries(subsequentGetter, 1) : schemas; } private static IEnumerable FlattenIfRequired(this IList schemas, Func> subsequentGetter) { - return schemas.FlattenSchemaIfRequired(subsequentGetter).Where(static x => !string.IsNullOrEmpty(x.Title)).Select(static x => x.Title); + return schemas.FlattenSchemaIfRequired(subsequentGetter).SelectMany(static x => x.GetSchemaNames()); } - public static string GetSchemaName(this OpenApiSchema schema) + public static string GetSchemaName(this OpenApiSchema schema, bool directOnly = false) { - return schema.GetSchemaNames().LastOrDefault()?.TrimStart('$') ?? string.Empty;// OData $ref + return schema.GetSchemaNames(directOnly).LastOrDefault()?.TrimStart('$') ?? string.Empty;// OData $ref } public static bool IsReferencedSchema(this OpenApiSchema schema) @@ -61,13 +57,17 @@ public static bool IsArray(this OpenApiSchema? schema) (schema.Items.IsComposedEnum() || schema.Items.IsEnum() || schema.Items.IsSemanticallyMeaningful() || - FlattenEmptyEntries(new OpenApiSchema[] { schema.Items }, static x => x.AnyOf.Union(x.AllOf).Union(x.OneOf).ToList(), 1).FirstOrDefault() is OpenApiSchema flat && flat.IsSemanticallyMeaningful()); + FlattenEmptyEntries([schema.Items], static x => x.AnyOf.Union(x.AllOf).Union(x.OneOf).ToList(), 1).FirstOrDefault() is OpenApiSchema flat && flat.IsSemanticallyMeaningful()); } - public static bool IsObject(this OpenApiSchema? schema) + public static bool IsObjectType(this OpenApiSchema? schema) { return "object".Equals(schema?.Type, StringComparison.OrdinalIgnoreCase); } + public static bool HasAnyProperty(this OpenApiSchema? schema) + { + return schema?.Properties is { Count: > 0 }; + } public static bool IsInclusiveUnion(this OpenApiSchema? schema) { return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > 1; @@ -77,25 +77,33 @@ public static bool IsInclusiveUnion(this OpenApiSchema? schema) public static bool IsInherited(this OpenApiSchema? schema) { if (schema is null) return false; - var meaningfulSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).Where(static x => x.IsSemanticallyMeaningful()).ToArray(); - return meaningfulSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && meaningfulSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1; + var meaningfulMemberSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).Where(static x => x.IsSemanticallyMeaningful()).ToArray(); + var isRootSchemaMeaningful = schema.IsSemanticallyMeaningful(); + return meaningfulMemberSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && + (meaningfulMemberSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1 || + isRootSchemaMeaningful); } - internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema) + internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema, HashSet? schemasToExclude = default) { if (schema is null) return null; if (!schema.IsIntersection()) return schema; var result = new OpenApiSchema(schema); result.AllOf.Clear(); - var meaningfulSchemas = schema.AllOf.Where(static x => x.IsSemanticallyMeaningful()).Select(MergeIntersectionSchemaEntries).Where(x => x is not null).OfType(); - meaningfulSchemas.SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value)); + var meaningfulSchemas = schema.AllOf + .Where(static x => x.IsSemanticallyMeaningful() || x.AllOf.Any()) + .Select(x => MergeIntersectionSchemaEntries(x, schemasToExclude)) + .Where(x => x is not null && (schemasToExclude is null || !schemasToExclude.Contains(x))) + .OfType() + .ToArray(); + meaningfulSchemas.FlattenEmptyEntries(static x => x.AllOf).Union(meaningfulSchemas).SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value)); return result; } public static bool IsIntersection(this OpenApiSchema? schema) { - var meaningfulSchemas = schema?.AllOf?.Where(static x => x.IsSemanticallyMeaningful()); - return meaningfulSchemas?.Count() > 3 || meaningfulSchemas?.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) > 1 || meaningfulSchemas?.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) > 1; + var meaningfulSchemas = schema?.AllOf?.Where(static x => x.IsSemanticallyMeaningful()).ToArray(); + return meaningfulSchemas?.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) > 1 || meaningfulSchemas?.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) > 1; } public static bool IsExclusiveUnion(this OpenApiSchema? schema) @@ -136,7 +144,7 @@ public static bool IsComposedEnum(this OpenApiSchema schema) public static bool IsSemanticallyMeaningful(this OpenApiSchema schema, bool ignoreNullableObjects = false) { if (schema is null) return false; - return schema.Properties.Any() || + return schema.HasAnyProperty() || schema.Enum is { Count: > 0 } || schema.Items != null || (!string.IsNullOrEmpty(schema.Type) && @@ -171,11 +179,11 @@ public static IEnumerable GetSchemaReferenceIds(this OpenApiSchema schem return result.Distinct(); } - return Enumerable.Empty(); + return []; } private static IEnumerable FlattenEmptyEntries(this IEnumerable schemas, Func> subsequentGetter, int? maxDepth = default) { - if (schemas == null) return Enumerable.Empty(); + if (schemas == null) return []; ArgumentNullException.ThrowIfNull(subsequentGetter); if ((maxDepth ?? 1) <= 0) @@ -186,7 +194,7 @@ private static IEnumerable FlattenEmptyEntries(this IEnumerable 0) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 5985e0534e..2de899ec17 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1551,43 +1551,56 @@ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, O TypeDefinition = codeDeclaration, }; } - private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody) + private CodeType CreateInheritedModelDeclarationAndType(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { - var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf); - CodeElement? codeDeclaration = null; - var className = string.Empty; - var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); - foreach (var currentSchema in allOfs) + return new CodeType { - var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); - var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); - var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace?.FindOrAddNamespace(shortestNamespaceName); - className = (currentSchema.GetSchemaName() is string cName && !string.IsNullOrEmpty(cName) ? + TypeDefinition = CreateInheritedModelDeclaration(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema), + }; + } + private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) + { + var flattenedAllOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).ToArray(); + var referenceId = schema.Reference?.Id; + var className = (schema.GetSchemaName(true) is string cName && !string.IsNullOrEmpty(cName) ? cName : - currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: schema, requestBody: isRequestBody)) + (!string.IsNullOrEmpty(typeNameForInlineSchema) ? + typeNameForInlineSchema : + currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: schema, requestBody: isRequestBody))) .CleanupSymbolName(); - if (shortestNamespace != null) - codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass); - } - if (codeDeclaration is CodeClass currentClass && - !currentClass.Documentation.DescriptionAvailable && + var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); + var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); + if (rootNamespace is null) + throw new InvalidOperationException("Root namespace is not set"); + var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace.FindOrAddNamespace(shortestNamespaceName); + var inlineSchema = Array.Find(flattenedAllOfs, static x => !x.IsReferencedSchema()); + var referencedSchema = Array.Find(flattenedAllOfs, static x => x.IsReferencedSchema()); + var rootSchemaHasProperties = schema.HasAnyProperty(); + var codeDeclaration = (rootSchemaHasProperties, inlineSchema, referencedSchema) switch + { + // greatest parent type + (true, null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace), + // inline schema + referenced schema + (false, not null, not null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, string.Empty)), + // properties + referenced schema + (true, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, string.Empty)), + // properties + inline schema + (true, not null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, inlineSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + // empty schema + referenced schema + (false, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, referencedSchema, className, shortestNamespace), + // empty schema + inline schema + (false, not null, null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace), + // meaningless scenarios + (false, null, null) or (true, not null, not null) => throw new InvalidOperationException("invalid inheritance case"), + + }; + if (codeDeclaration is not CodeClass currentClass) throw new InvalidOperationException("Inheritance is only supported for classes"); + if (!currentClass.Documentation.DescriptionAvailable && string.IsNullOrEmpty(schema.AllOf.LastOrDefault()?.Description) && !string.IsNullOrEmpty(schema.Description)) currentClass.Documentation.DescriptionTemplate = schema.Description.CleanupDescription(); // the last allof entry often is not a reference and doesn't have a description. - return new CodeType - { - TypeDefinition = codeDeclaration, - }; - } - private static string? GetReferenceIdFromOriginalSchema(OpenApiSchema schema, OpenApiSchema parentSchema) - { - var title = schema.Title; - if (!string.IsNullOrEmpty(schema.Reference?.Id)) return schema.Reference.Id; - if (string.IsNullOrEmpty(title)) return string.Empty; - if (parentSchema.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Reference.Id; - if (parentSchema.Items?.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Items.Reference.Id; - return parentSchema.GetSchemaReferenceIds().FirstOrDefault(refId => refId.EndsWith(title, StringComparison.OrdinalIgnoreCase)); + return currentClass; } private CodeTypeBase CreateComposedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string suffixForInlineSchema, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { @@ -1596,7 +1609,7 @@ private CodeTypeBase CreateComposedModelDeclaration(OpenApiUrlTreeNode currentNo if (typesCount == 1 && schema.Nullable && schema.IsInclusiveUnion() || // nullable on the root schema outside of anyOf typesCount == 2 && (schema.AnyOf?.Any(static x => // nullable on a schema in the anyOf x.Nullable && - !x.Properties.Any() && + !x.HasAnyProperty() && !x.IsExclusiveUnion() && !x.IsInclusiveUnion() && !x.IsInherited() && @@ -1675,7 +1688,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope if (schema.IsInherited()) { - return CreateInheritedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody); + return CreateInheritedModelDeclarationAndType(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); } if (schema.IsIntersection() && schema.MergeIntersectionSchemaEntries() is OpenApiSchema mergedSchema) @@ -1690,7 +1703,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope return CreateComposedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); } - if (schema.IsObject() || schema.Properties.Any() || schema.IsEnum() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) + if (schema.IsObjectType() || schema.HasAnyProperty() || schema.IsEnum() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) { // no inheritance or union type, often empty definitions with only additional properties are used as property bags. return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody); @@ -1739,13 +1752,22 @@ private CodeNamespace GetSearchNamespace(OpenApiUrlTreeNode currentNode, CodeNam return currentNamespace; } private ConcurrentDictionary classLifecycles = new(StringComparer.OrdinalIgnoreCase); - private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null) + private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null, OpenApiSchema? parentSchemaToExcludeForIntersections = null) { if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is not CodeElement existingDeclaration) // we can find it in the components { if (AddEnumDeclaration(currentNode, schema, declarationName, currentNamespace) is CodeEnum enumDeclaration) return enumDeclaration; + if (schema.IsIntersection() && + (parentSchemaToExcludeForIntersections is null ? + schema.MergeIntersectionSchemaEntries() : + schema.MergeIntersectionSchemaEntries([parentSchemaToExcludeForIntersections])) is OpenApiSchema mergedSchema && + AddModelDeclarationIfDoesntExist(currentNode, mergedSchema, declarationName, currentNamespace, inheritsFrom) is CodeClass createdClass) + { + // multiple allOf entries that do not translate to inheritance + return createdClass; + } return AddModelClass(currentNode, schema, declarationName, currentNamespace, inheritsFrom); } return existingDeclaration; @@ -1824,8 +1846,9 @@ private CodeNamespace GetShortestNamespace(CodeNamespace currentNamespace, OpenA } private CodeClass AddModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null) { - if (inheritsFrom == null && schema.AllOf.FirstOrDefault(static x => x.Reference != null) is OpenApiSchema parentSchema) + if (inheritsFrom == null && schema.AllOf.Where(static x => x.Reference != null).ToArray() is { Length: 1 } referencedSchemas) {// any non-reference would be the current class in some description styles + var parentSchema = referencedSchemas[0]; var parentClassNamespace = GetShortestNamespace(currentNamespace, parentSchema); inheritsFrom = (CodeClass)AddModelDeclarationIfDoesntExist(currentNode, parentSchema, parentSchema.GetSchemaName().CleanupSymbolName(), parentClassNamespace); } @@ -2068,7 +2091,12 @@ internal static void AddDiscriminatorMethod(CodeClass newClass, string discrimin } var className = currentNode.GetClassName(config.StructuredMimeTypes, schema: discriminatorSchema).CleanupSymbolName(); var shouldInherit = discriminatorSchema.AllOf.Any(x => currentSchema.Reference?.Id.Equals(x.Reference?.Id, StringComparison.OrdinalIgnoreCase) ?? false); - var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null); + if (baseClass is not null && shouldInherit && !discriminatorSchema.IsInherited()) + { + logger.LogWarning("Discriminator {ComponentKey} is not inherited from {ClassName}.", componentKey, baseClass.Name); + return null; + } + var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null, currentSchema); return new CodeType { TypeDefinition = codeClass, @@ -2105,7 +2133,7 @@ private Dictionary CollectAllProperties(OpenApiSchema sch Dictionary result = schema.Properties?.ToDictionary(static x => x.Key, static x => x.Value, StringComparer.Ordinal) ?? new(StringComparer.Ordinal); if (schema.AllOf?.Any() ?? false) { - foreach (var supProperty in schema.AllOf.Where(static x => x.IsObject() && !x.IsReferencedSchema() && x.Properties is not null).SelectMany(static x => x.Properties)) + foreach (var supProperty in schema.AllOf.Where(static x => !x.IsReferencedSchema() && x.HasAnyProperty()).SelectMany(static x => x.Properties)) { result.Add(supProperty.Key, supProperty.Value); } diff --git a/src/Kiota.Builder/Validation/MissingDiscriminator.cs b/src/Kiota.Builder/Validation/MissingDiscriminator.cs index ad12d74ea2..c889f5cf5a 100644 --- a/src/Kiota.Builder/Validation/MissingDiscriminator.cs +++ b/src/Kiota.Builder/Validation/MissingDiscriminator.cs @@ -35,7 +35,7 @@ private static void ValidateSchema(OpenApiSchema schema, IValidationContext cont { if (!schema.IsInclusiveUnion() && !schema.IsExclusiveUnion()) return; - if (schema.AnyOf.All(static x => !x.IsObject()) && schema.OneOf.All(static x => !x.IsObject())) + if (schema.AnyOf.All(static x => !x.IsObjectType()) && schema.OneOf.All(static x => !x.IsObjectType())) return; if (string.IsNullOrEmpty(schema.GetDiscriminatorPropertyName()) || !schema.GetDiscriminatorMappings(idx).Any()) context.CreateWarning(nameof(MissingDiscriminator), $"The schema {address} is a polymorphic type but does not define a discriminator. This will result in a serialization errors."); diff --git a/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs b/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs index 0257c53902..78a4b31572 100644 --- a/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs +++ b/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs @@ -23,9 +23,9 @@ public UrlFormEncodedComplex() : base(nameof(UrlFormEncodedComplex), static (con private static void ValidateSchema(OpenApiSchema schema, IValidationContext context, string operationId, string schemaName) { if (schema == null) return; - if (!schema.IsObject()) + if (!schema.IsObjectType()) context.CreateWarning(nameof(UrlFormEncodedComplex), $"The operation {operationId} has a {schemaName} which is not an object type. This is not supported by Kiota and serialization will fail."); - if (schema.Properties.Any(static x => x.Value.IsObject())) + if (schema.Properties.Any(static x => x.Value.IsObjectType())) context.CreateWarning(nameof(UrlFormEncodedComplex), $"The operation {operationId} has a {schemaName} with a complex properties and the url form encoded content type. This is not supported by Kiota and serialization of complex properties will fail."); } } diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 19accd89df..08d46a13c7 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -43,17 +43,19 @@ public void Defensive() Assert.False(OpenApiSchemaExtensions.IsInclusiveUnion(null)); Assert.False(OpenApiSchemaExtensions.IsExclusiveUnion(null)); Assert.False(OpenApiSchemaExtensions.IsArray(null)); - Assert.False(OpenApiSchemaExtensions.IsObject(null)); + Assert.False(OpenApiSchemaExtensions.IsObjectType(null)); + Assert.False(OpenApiSchemaExtensions.HasAnyProperty(null)); Assert.False(OpenApiSchemaExtensions.IsReferencedSchema(null)); Assert.Null(OpenApiSchemaExtensions.MergeIntersectionSchemaEntries(null)); Assert.False(new OpenApiSchema { Reference = null }.IsReferencedSchema()); Assert.False(new OpenApiSchema { Type = null }.IsArray()); - Assert.False(new OpenApiSchema { Type = null }.IsObject()); + Assert.False(new OpenApiSchema { Type = null }.IsObjectType()); Assert.False(new OpenApiSchema { AnyOf = null }.IsInclusiveUnion()); Assert.False(new OpenApiSchema { AllOf = null }.IsInherited()); Assert.False(new OpenApiSchema { AllOf = null }.IsIntersection()); Assert.False(new OpenApiSchema { OneOf = null }.IsExclusiveUnion()); + Assert.False(new OpenApiSchema { Properties = null }.HasAnyProperty()); var original = new OpenApiSchema { AllOf = null }; Assert.Equal(original, original.MergeIntersectionSchemaEntries()); @@ -84,87 +86,205 @@ public void LocalReferencesAreSupported() Assert.True(mockSchema.IsReferencedSchema()); } [Fact] - public void GetSchemaNameAllOf() + public void GetSchemaNameAllOfTitleEmpty() { var schema = new OpenApiSchema { - AllOf = new List { - new() { + AllOf = [ + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameAllOfNested() + public void GetSchemaNameAllOfReference() { var schema = new OpenApiSchema { - AllOf = new List { - new() { - AllOf = new List { - new() { + AllOf = [ + new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + new() + { + Reference = new() + { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameAllOfNestedTitleEmpty() + { + var schema = new OpenApiSchema + { + AllOf = [ + new() + { + AllOf = [ + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } - } + ] } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameAnyOf() + public void GetSchemaNameAllOfNestedReference() { var schema = new OpenApiSchema { - AnyOf = new List { - new() { + AllOf = [ + new() + { + AllOf = [ + new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + new() + { + Reference = new() + { + Id = "microsoft.graph.user" + } + } + ] + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameAnyOfTitleEmpty() + { + var schema = new OpenApiSchema + { + AnyOf = [ + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameOneOf() + public void GetSchemaNameAnyOfReference() { var schema = new OpenApiSchema { - OneOf = new List { - new() { + AnyOf = [ + new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + new() + { + Reference = new() + { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameOneOfTitleEmpty() + { + var schema = new OpenApiSchema + { + OneOf = [ + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameOneOfReference() + { + var schema = new OpenApiSchema + { + OneOf = [ + new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + new() + { + Reference = new() + { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); } [Fact] - public void GetSchemaNameItems() + public void GetSchemaNameItemsTitleEmpty() { var schema = new OpenApiSchema { @@ -174,20 +294,51 @@ public void GetSchemaNameItems() }, }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Equal("microsoft.graph.entity", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameItemsReference() + { + var schema = new OpenApiSchema + { + Items = new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Equal("entity", schema.GetSchemaName()); Assert.Single(names); } [Fact] - public void GetSchemaNameTitle() + public void GetSchemaNameTitleEmpty() { var schema = new OpenApiSchema { Title = "microsoft.graph.entity" }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Equal("microsoft.graph.entity", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameReference() + { + var schema = new OpenApiSchema + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Equal("entity", schema.GetSchemaName()); Assert.Single(names); } [Fact] diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 82dabd2d6a..66dd9e4f5f 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -1878,7 +1878,7 @@ public void Inline_Property_Inheritance_Is_Supported() Type = "object", Properties = new Dictionary { { - "info", new OpenApiSchema { + "info2", new OpenApiSchema { Type = "object", Properties = new Dictionary { { @@ -1890,9 +1890,9 @@ public void Inline_Property_Inheritance_Is_Supported() } } }, - AllOf = new List { + AllOf = [ resourceSchema, - } + ] } } } @@ -1923,12 +1923,12 @@ public void Inline_Property_Inheritance_Is_Supported() var itemsNS = codeModel.FindNamespaceByName("ApiSdk.resource.item"); var responseClass = itemsNS.FindChildByName("ResourceGetResponse"); var derivedResourceClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource"); - var derivedResourceInfoClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource_info"); + var derivedResourceInfoClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource_info2"); Assert.NotNull(resourceClass); Assert.NotNull(derivedResourceClass); - Assert.NotNull(derivedResourceClass.StartBlock); + Assert.NotNull(derivedResourceClass.StartBlock.Inherits); Assert.Equal(derivedResourceClass.StartBlock.Inherits.TypeDefinition, resourceClass); Assert.NotNull(derivedResourceInfoClass); Assert.NotNull(responseClass); @@ -7396,7 +7396,7 @@ public async Task InheritanceWithAllOfInBaseType() Assert.NotNull(codeModel.FindChildByName("Group")); } [Fact] - public async Task InheritanceWithAllOfWith3Parts() + public async Task InheritanceWithAllOfWith3Parts3SchemaChildClass() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); await using var fs = await GetDocumentStream(@"openapi: 3.0.1 @@ -7411,17 +7411,22 @@ public async Task InheritanceWithAllOfWith3Parts() get: responses: '200': - description: Example response content: application/json: schema: $ref: '#/components/schemas/microsoft.graph.directoryObject' + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' components: schemas: microsoft.graph.directoryObject: - title: 'directoryObject' required: ['@odata.type'] - type: 'object' properties: '@odata.type': type: 'string' @@ -7429,21 +7434,357 @@ public async Task InheritanceWithAllOfWith3Parts() discriminator: propertyName: '@odata.type' mapping: - '#microsoft.graph.user': '#/components/schemas/microsoft.graph.user' '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' + microsoft.graph.groupFacet1: + properties: + facetprop1: + type: 'string' + microsoft.graph.groupFacet2: + properties: + facetprop2: + type: 'string' + microsoft.graph.group: + title: 'group' + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + - '$ref': '#/components/schemas/microsoft.graph.groupFacet1' + - '$ref': '#/components/schemas/microsoft.graph.groupFacet2'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var directoryObjectClass = codeModel.FindChildByName("DirectoryObject"); + Assert.Null(directoryObjectClass.StartBlock.Inherits); + Assert.NotNull(directoryObjectClass); + var groupClass = codeModel.FindChildByName("Group"); + Assert.NotNull(groupClass); + Assert.Equal(4, groupClass.Properties.Count()); + Assert.Null(groupClass.StartBlock.Inherits); + Assert.Single(groupClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task NestedIntersectionTypeAllOf() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.3 +info: + title: Model Registry REST API + version: v1alpha2 + description: REST API for Model Registry to create and manage ML model metadata + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +servers: + - url: 'https://localhost:8080' + - url: 'http://localhost:8080' +paths: + /api/model_registry/v1alpha2/registered_models: + summary: Path used to manage the list of registeredmodels. + description: >- + The REST endpoint/path used to list and create zero or more `RegisteredModel` entities. This path contains a `GET` and `POST` operation to perform the list and create tasks, respectively. + get: + responses: + '200': + $ref: '#/components/responses/RegisteredModelListResponse' + summary: List All RegisteredModels + description: Gets a list of all `RegisteredModel` entities. +components: + schemas: + BaseResource: + type: object + properties: + id: + format: int64 + description: Output only. The unique server generated id of the resource. + type: number + readOnly: true + allOf: + - $ref: '#/components/schemas/BaseResourceCreate' + BaseResourceCreate: + type: object + properties: + name: + description: |- + The client provided name of the artifact. This field is optional. If set, + it must be unique among all the artifacts of the same artifact type within + a database instance and cannot be changed once set. + type: string + BaseResourceList: + required: + - size + type: object + properties: + size: + format: int32 + description: Number of items in result list. + type: integer + RegisteredModel: + description: A registered model in model registry. A registered model has ModelVersion children. + allOf: + - $ref: '#/components/schemas/BaseResource' + - $ref: '#/components/schemas/RegisteredModelCreate' + RegisteredModelCreate: + description: A registered model in model registry. A registered model has ModelVersion children. + allOf: + - $ref: '#/components/schemas/BaseResourceCreate' + RegisteredModelList: + description: List of RegisteredModels. + type: object + allOf: + - $ref: '#/components/schemas/BaseResourceList' + - type: object + properties: + items: + description: '' + type: array + items: + $ref: '#/components/schemas/RegisteredModel' + readOnly: false + responses: + RegisteredModelListResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisteredModelList' + description: A response containing a list of `RegisteredModel` entities.\"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var registeredModelClass = codeModel.FindChildByName("RegisteredModel"); + Assert.Null(registeredModelClass.StartBlock.Inherits); + Assert.NotNull(registeredModelClass); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Name.Equals("name", StringComparison.OrdinalIgnoreCase))); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Name.Equals("id", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith3Parts3SchemaParentClass() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /directoryObject: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.directoryObject' + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + required: ['@odata.type'] + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObjectFacet1' + - '$ref': '#/components/schemas/microsoft.graph.directoryObjectFacet2' + discriminator: + propertyName: '@odata.type' + mapping: + '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' + microsoft.graph.directoryObjectFacet1: + properties: + facetprop1: + type: 'string' + microsoft.graph.directoryObjectFacet2: + properties: + facetprop2: + type: 'string' microsoft.graph.group: + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var directoryObjectClass = codeModel.FindChildByName("DirectoryObject"); + Assert.NotNull(directoryObjectClass); + Assert.Null(directoryObjectClass.StartBlock.Inherits); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + var groupClass = codeModel.FindChildByName("Group"); + Assert.NotNull(groupClass); + Assert.Single(groupClass.Properties); + Assert.NotNull(groupClass.StartBlock.Inherits); + Assert.Empty(groupClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith2Parts1Schema1InlineNoDiscriminator() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + title: 'directoryObject' + required: ['@odata.type'] + type: 'object' + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + microsoft.graph.group: + type: 'object' allOf: - '$ref': '#/components/schemas/microsoft.graph.directoryObject' - title: 'group part 1' type: 'object' properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var resultClass = codeModel.FindChildByName("Group"); + Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(resultClass.Properties); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith1Part1SchemaAndPropertiesNoDiscriminator() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + title: 'directoryObject' + required: ['@odata.type'] + type: 'object' + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + microsoft.graph.group: + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + type: 'object' + properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var resultClass = codeModel.FindChildByName("Group"); + Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(resultClass.Properties); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task InheritanceWithAllOfWith3Parts1Schema2Inline(bool reverseOrder) + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /directoryObject: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.directoryObject' +components: + schemas: + microsoft.graph.directoryObject: + required: ['@odata.type'] + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + discriminator: + propertyName: '@odata.type' + mapping: + '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' + microsoft.graph.group: + allOf:" + + (reverseOrder ? "" : @" + - '$ref': '#/components/schemas/microsoft.graph.directoryObject'") + @" + - properties: groupprop1: type: 'string' - - title: 'group part 2' - type: 'object' - properties: + - properties: groupprop2: - type: 'string'"); + type: 'string'" + (!reverseOrder ? "" : @" + - '$ref': '#/components/schemas/microsoft.graph.directoryObject'")); var mockLogger = new Mock>(); var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); @@ -7451,9 +7792,60 @@ public async Task InheritanceWithAllOfWith3Parts() var codeModel = builder.CreateSourceModel(node); var resultClass = codeModel.FindChildByName("Group"); Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); Assert.Equal(2, resultClass.Properties.Count()); - Assert.Single(resultClass.Properties.Where(x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); - Assert.Single(resultClass.Properties.Where(x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(resultClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithoutObjectTypeHasAllProperties() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.3 +servers: + - url: 'https://example.com' +info: + title: example + version: 0.0.1 +paths: + /path: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/outerPayload' + responses: + '201': + description: Created + content: + application/json: + schema: + type: string + +components: + schemas: + outerPayload: + allOf: + - $ref: '#/components/schemas/innerPayload' + - properties: + someField: + type: string + innerPayload: + properties: + anotherField: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var outerPayloadClass = codeModel.FindChildByName("outerPayload"); + Assert.NotNull(outerPayloadClass); + Assert.Equal("innerPayload", outerPayloadClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(outerPayloadClass.Properties); + Assert.Single(outerPayloadClass.Properties.Where(static x => x.Name.Equals("someField", StringComparison.OrdinalIgnoreCase))); } [Fact] public async Task EnumsWithNullableDoesNotResultInInlineType()