Skip to content

Commit

Permalink
- fixes inheritance from intersection type
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
baywet committed May 7, 2024
1 parent d32ec68 commit 127d16d
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 39 deletions.
8 changes: 6 additions & 2 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,17 @@ public static bool IsInherited(this OpenApiSchema? schema)
isRootSchemaMeaningful);
}

internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema)
internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema, HashSet<OpenApiSchema>? 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<OpenApiSchema>();
var meaningfulSchemas = schema.AllOf
.Where(static x => x.IsSemanticallyMeaningful())
.Select(x => MergeIntersectionSchemaEntries(x, schemasToExclude))
.Where(x => x is not null && (schemasToExclude is null || !schemasToExclude.Contains(x)))
.OfType<OpenApiSchema>();
meaningfulSchemas.SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value));
return result;
}
Expand Down
29 changes: 22 additions & 7 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,9 +1554,11 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode,
var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)
.Union(isRootSchemaMeaningful ?
[schema] :
Array.Empty<OpenApiSchema>());
CodeElement? codeDeclaration = null;
Array.Empty<OpenApiSchema>())
.ToArray();
CodeClass? codeDeclaration = null;
var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema);
OpenApiSchema? previousSchema = null;
foreach (var currentSchema in allOfs)
{
var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema);
Expand All @@ -1568,10 +1570,14 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode,
typeNameForInlineSchema :
currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: currentSchema, requestBody: isRequestBody)))
.CleanupSymbolName();
if (shortestNamespace != null)
codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass);
if (shortestNamespace != null &&
AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration, previousSchema) is CodeClass addedDeclaration)
{
codeDeclaration = addedDeclaration;
previousSchema = currentSchema;
}
}
if (codeDeclaration is CodeClass currentClass &&
if (codeDeclaration is { } currentClass &&
!currentClass.Documentation.DescriptionAvailable &&
string.IsNullOrEmpty(schema.AllOf.LastOrDefault()?.Description) &&
!string.IsNullOrEmpty(schema.Description))
Expand Down Expand Up @@ -1741,13 +1747,22 @@ private CodeNamespace GetSearchNamespace(OpenApiUrlTreeNode currentNode, CodeNam
return currentNamespace;
}
private ConcurrentDictionary<string, ModelClassBuildLifecycle> 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;
Expand Down Expand Up @@ -2077,7 +2092,7 @@ internal static void AddDiscriminatorMethod(CodeClass newClass, string discrimin
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);
var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null, currentSchema);
return new CodeType
{
TypeDefinition = codeClass,
Expand Down
132 changes: 102 additions & 30 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7396,7 +7396,7 @@ public async Task InheritanceWithAllOfInBaseType()
Assert.NotNull(codeModel.FindChildByName<CodeClass>("Group"));
}
[Fact]
public async Task InheritanceWithAllOfWith3Parts3Schema()
public async Task InheritanceWithAllOfWith3Parts3SchemaChildClass()
{
var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
await using var fs = await GetDocumentStream(@"openapi: 3.0.1
Expand Down Expand Up @@ -7426,9 +7426,7 @@ public async Task InheritanceWithAllOfWith3Parts3Schema()
components:
schemas:
microsoft.graph.directoryObject:
title: 'directoryObject'
required: ['@odata.type']
type: 'object'
properties:
'@odata.type':
type: 'string'
Expand All @@ -7438,16 +7436,12 @@ public async Task InheritanceWithAllOfWith3Parts3Schema()
mapping:
'#microsoft.graph.group': '#/components/schemas/microsoft.graph.group'
microsoft.graph.groupFacet1:
title: 'group part 1'
type: 'object'
properties:
groupprop1:
facetprop1:
type: 'string'
microsoft.graph.groupFacet2:
title: 'group part 2'
type: 'object'
properties:
groupprop2:
facetprop2:
type: 'string'
microsoft.graph.group:
title: 'group'
Expand All @@ -7461,15 +7455,95 @@ public async Task InheritanceWithAllOfWith3Parts3Schema()
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var directoryObjectClass = codeModel.FindChildByName<CodeClass>("DirectoryObject");
Assert.Null(directoryObjectClass.StartBlock.Inherits);
Assert.NotNull(directoryObjectClass);
var resultClass = codeModel.FindChildByName<CodeClass>("Group");
Assert.NotNull(resultClass);
Assert.Equal(4, resultClass.Properties.Count());
Assert.Null(resultClass.StartBlock.Inherits);
Assert.Single(resultClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData));
Assert.Single(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)));
var groupClass = codeModel.FindChildByName<CodeClass>("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 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<ILogger<KiotaBuilder>>();
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<CodeClass>("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<CodeClass>("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()
Expand Down Expand Up @@ -7569,8 +7643,10 @@ public async Task InheritanceWithAllOfWith1Part1SchemaAndPropertiesNoDiscriminat
Assert.Single(resultClass.Properties);
Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase)));
}
[Fact]
public async Task InheritanceWithAllOfWith3Parts1Schema2Inline()
[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
Expand All @@ -7592,9 +7668,7 @@ public async Task InheritanceWithAllOfWith3Parts1Schema2Inline()
components:
schemas:
microsoft.graph.directoryObject:
title: 'directoryObject'
required: ['@odata.type']
type: 'object'
properties:
'@odata.type':
type: 'string'
Expand All @@ -7604,18 +7678,16 @@ public async Task InheritanceWithAllOfWith3Parts1Schema2Inline()
mapping:
'#microsoft.graph.group': '#/components/schemas/microsoft.graph.group'
microsoft.graph.group:
allOf:
- '$ref': '#/components/schemas/microsoft.graph.directoryObject'
- title: 'group part 1'
type: 'object'
properties:
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<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient);
var document = await builder.CreateOpenApiDocumentAsync(fs);
Expand Down

0 comments on commit 127d16d

Please sign in to comment.