зеркало из https://github.com/microsoft/kiota.git
merge schemas in allOf when generating a sliced OpenAPI document. (#5308)
* merge schemas in allOf when generating a sliced OpenAPI document. * add test for object schema merge * add null checks for allOf inlining algorithm * add check for allOf types with different values * fix schema type validation * remove comment * add any of and one of * Add test for normal schema being unchanged * apply code suggestions * use SelectMany for AllOf flattening loop
This commit is contained in:
Родитель
915c31f8b4
Коммит
5fdd2615bf
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -12,7 +11,6 @@ using Kiota.Builder.Extensions;
|
|||
using Kiota.Builder.OpenApiExtensions;
|
||||
using Microsoft.Kiota.Abstractions.Extensions;
|
||||
using Microsoft.OpenApi.ApiManifest;
|
||||
using Microsoft.OpenApi.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Services;
|
||||
using Microsoft.OpenApi.Writers;
|
||||
|
@ -60,6 +58,7 @@ public partial class PluginsGenerationService
|
|||
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
|
||||
var descriptionWriter = new OpenApiYamlWriter(fileWriter);
|
||||
var trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(OAIDocument);
|
||||
trimmedPluginDocument = InlineRequestBodyAllOf(trimmedPluginDocument);
|
||||
trimmedPluginDocument.SerializeAsV3(descriptionWriter);
|
||||
descriptionWriter.Flush();
|
||||
|
||||
|
@ -104,6 +103,126 @@ public partial class PluginsGenerationService
|
|||
}
|
||||
}
|
||||
|
||||
private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDocument)
|
||||
{
|
||||
if (openApiDocument.Paths is null) return openApiDocument;
|
||||
var contentItems = openApiDocument.Paths.Values.Where(static x => x?.Operations is not null)
|
||||
.SelectMany(static x => x.Operations.Values.Where(static x => x?.RequestBody?.Content is not null)
|
||||
.SelectMany(static x => x.RequestBody.Content.Values));
|
||||
foreach (var contentItem in contentItems)
|
||||
{
|
||||
var schema = contentItem.Schema;
|
||||
// Merge all schemas in allOf `schema.MergeAllOfSchemaEntries()` doesn't seem to do the right thing.
|
||||
schema = MergeAllOfInSchema(schema);
|
||||
schema = SelectFirstAnyOfOrOneOf(schema);
|
||||
contentItem.Schema = schema;
|
||||
}
|
||||
|
||||
return openApiDocument;
|
||||
|
||||
static OpenApiSchema? SelectFirstAnyOfOrOneOf(OpenApiSchema? schema)
|
||||
{
|
||||
if (schema?.AnyOf is not { Count: > 0 } && schema?.OneOf is not { Count: > 0 }) return schema;
|
||||
OpenApiSchema newSchema;
|
||||
if (schema.AnyOf is { Count: > 0 })
|
||||
{
|
||||
newSchema = schema.AnyOf[0];
|
||||
}
|
||||
else if (schema.OneOf is { Count: > 0 })
|
||||
{
|
||||
newSchema = schema.OneOf[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
newSchema = schema;
|
||||
}
|
||||
return newSchema;
|
||||
}
|
||||
static OpenApiSchema? MergeAllOfInSchema(OpenApiSchema? schema)
|
||||
{
|
||||
if (schema?.AllOf is not { Count: > 0 }) return schema;
|
||||
var newSchema = new OpenApiSchema();
|
||||
foreach (var apiSchema in schema.AllOf)
|
||||
{
|
||||
if (apiSchema.Title is not null) newSchema.Title = apiSchema.Title;
|
||||
if (!string.IsNullOrEmpty(apiSchema.Type))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(newSchema.Type) && newSchema.Type != apiSchema.Type)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The schemas in allOf cannot have different types: '{newSchema.Type}' and '{apiSchema.Type}'.");
|
||||
}
|
||||
newSchema.Type = apiSchema.Type;
|
||||
}
|
||||
if (apiSchema.Format is not null) newSchema.Format = apiSchema.Format;
|
||||
if (!string.IsNullOrEmpty(apiSchema.Description)) newSchema.Description = apiSchema.Description;
|
||||
if (apiSchema.Maximum is not null) newSchema.Maximum = apiSchema.Maximum;
|
||||
if (apiSchema.ExclusiveMaximum is not null) newSchema.ExclusiveMaximum = apiSchema.ExclusiveMaximum;
|
||||
if (apiSchema.Minimum is not null) newSchema.Minimum = apiSchema.Minimum;
|
||||
if (apiSchema.ExclusiveMinimum is not null) newSchema.ExclusiveMinimum = apiSchema.ExclusiveMinimum;
|
||||
if (apiSchema.MaxLength is not null) newSchema.MaxLength = apiSchema.MaxLength;
|
||||
if (apiSchema.MinLength is not null) newSchema.MinLength = apiSchema.MinLength;
|
||||
if (!string.IsNullOrEmpty(apiSchema.Pattern)) newSchema.Pattern = apiSchema.Pattern;
|
||||
if (apiSchema.MultipleOf is not null) newSchema.MultipleOf = apiSchema.MultipleOf;
|
||||
if (apiSchema.Default is not null) newSchema.Default = apiSchema.Default;
|
||||
if (apiSchema.ReadOnly) newSchema.ReadOnly = apiSchema.ReadOnly;
|
||||
if (apiSchema.WriteOnly) newSchema.WriteOnly = apiSchema.WriteOnly;
|
||||
if (apiSchema.Not is not null) newSchema.Not = apiSchema.Not;
|
||||
if (apiSchema.Required is { Count: > 0 })
|
||||
{
|
||||
foreach (var r in apiSchema.Required.Where(static r => !string.IsNullOrEmpty(r)))
|
||||
{
|
||||
newSchema.Required.Add(r);
|
||||
}
|
||||
}
|
||||
if (apiSchema.Items is not null) newSchema.Items = apiSchema.Items;
|
||||
if (apiSchema.MaxItems is not null) newSchema.MaxItems = apiSchema.MaxItems;
|
||||
if (apiSchema.MinItems is not null) newSchema.MinItems = apiSchema.MinItems;
|
||||
if (apiSchema.UniqueItems is not null) newSchema.UniqueItems = apiSchema.UniqueItems;
|
||||
if (apiSchema.Properties is not null)
|
||||
{
|
||||
foreach (var property in apiSchema.Properties)
|
||||
{
|
||||
newSchema.Properties.Add(property.Key, property.Value);
|
||||
}
|
||||
}
|
||||
if (apiSchema.MaxProperties is not null) newSchema.MaxProperties = apiSchema.MaxProperties;
|
||||
if (apiSchema.MinProperties is not null) newSchema.MinProperties = apiSchema.MinProperties;
|
||||
if (apiSchema.AdditionalPropertiesAllowed) newSchema.AdditionalPropertiesAllowed = true;
|
||||
if (apiSchema.AdditionalProperties is not null) newSchema.AdditionalProperties = apiSchema.AdditionalProperties;
|
||||
if (apiSchema.Discriminator is not null) newSchema.Discriminator = apiSchema.Discriminator;
|
||||
if (apiSchema.Example is not null) newSchema.Example = apiSchema.Example;
|
||||
if (apiSchema.Enum is not null)
|
||||
{
|
||||
foreach (var enumValue in apiSchema.Enum)
|
||||
{
|
||||
newSchema.Enum.Add(enumValue);
|
||||
}
|
||||
}
|
||||
if (apiSchema.Nullable) newSchema.Nullable = apiSchema.Nullable;
|
||||
if (apiSchema.ExternalDocs is not null) newSchema.ExternalDocs = apiSchema.ExternalDocs;
|
||||
if (apiSchema.Deprecated) newSchema.Deprecated = apiSchema.Deprecated;
|
||||
if (apiSchema.Xml is not null) newSchema.Xml = apiSchema.Xml;
|
||||
if (apiSchema.Extensions is not null)
|
||||
{
|
||||
foreach (var extension in apiSchema.Extensions)
|
||||
{
|
||||
newSchema.Extensions.Add(extension.Key, extension.Value);
|
||||
}
|
||||
}
|
||||
if (apiSchema.Reference is not null) newSchema.Reference = apiSchema.Reference;
|
||||
if (apiSchema.Annotations is not null)
|
||||
{
|
||||
foreach (var annotation in apiSchema.Annotations)
|
||||
{
|
||||
newSchema.Annotations.Add(annotation.Key, annotation.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newSchema;
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[^a-zA-Z0-9_]+", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)]
|
||||
private static partial Regex PluginNameCleanupRegex();
|
||||
|
||||
|
|
|
@ -549,4 +549,180 @@ components:
|
|||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
#region Validation
|
||||
|
||||
public static TheoryData<string, Action<OpenApiDocument, OpenApiDiagnostic>>
|
||||
ValidationSchemaTestInput()
|
||||
{
|
||||
return new TheoryData<string, Action<OpenApiDocument, OpenApiDiagnostic>>
|
||||
{
|
||||
// AllOf
|
||||
// simple disjoint
|
||||
{
|
||||
"""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf: [
|
||||
{type: string},
|
||||
{maxLength: 5}
|
||||
]
|
||||
""", (slicedDocument, _) =>
|
||||
{
|
||||
Assert.NotNull(slicedDocument);
|
||||
Assert.NotEmpty(slicedDocument.Paths);
|
||||
var schema = slicedDocument.Paths["/test"].Operations[OperationType.Post].RequestBody
|
||||
.Content["application/json"].Schema;
|
||||
Assert.Equal("string", schema.Type);
|
||||
Assert.Equal(5, schema.MaxLength);
|
||||
}
|
||||
},
|
||||
// objects
|
||||
{
|
||||
"""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf: [
|
||||
{type: object, properties: {a: {type: string}, b: {type: number}}},
|
||||
{type: object, properties: {c: {type: number}}}
|
||||
]
|
||||
""", (slicedDocument, _) =>
|
||||
{
|
||||
Assert.NotNull(slicedDocument);
|
||||
Assert.NotEmpty(slicedDocument.Paths);
|
||||
var schema = slicedDocument.Paths["/test"].Operations[OperationType.Post].RequestBody
|
||||
.Content["application/json"].Schema;
|
||||
Assert.Equal("object", schema.Type);
|
||||
Assert.Equal(3, schema.Properties.Count);
|
||||
}
|
||||
},
|
||||
// AnyOf
|
||||
{
|
||||
"""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
anyOf: [
|
||||
{type: object, properties: {a: {type: string}, b: {type: number}}},
|
||||
{type: object, properties: {c: {type: number}}}
|
||||
]
|
||||
""", (slicedDocument, _) =>
|
||||
{
|
||||
Assert.NotNull(slicedDocument);
|
||||
Assert.NotEmpty(slicedDocument.Paths);
|
||||
var schema = slicedDocument.Paths["/test"].Operations[OperationType.Post].RequestBody
|
||||
.Content["application/json"].Schema;
|
||||
Assert.Equal("object", schema.Type);
|
||||
Assert.Equal(2, schema.Properties.Count);
|
||||
}
|
||||
},
|
||||
// OneOf
|
||||
{
|
||||
"""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf: [
|
||||
{type: object, properties: {c: {type: number}}},
|
||||
{type: object, properties: {a: {type: string}, b: {type: number}}}
|
||||
]
|
||||
""", (slicedDocument, _) =>
|
||||
{
|
||||
Assert.NotNull(slicedDocument);
|
||||
Assert.NotEmpty(slicedDocument.Paths);
|
||||
var schema = slicedDocument.Paths["/test"].Operations[OperationType.Post].RequestBody
|
||||
.Content["application/json"].Schema;
|
||||
Assert.Equal("object", schema.Type);
|
||||
Assert.Single(schema.Properties);
|
||||
}
|
||||
},
|
||||
// normal schema
|
||||
{
|
||||
"""
|
||||
content:
|
||||
application/json:
|
||||
schema: {type: object, properties: {c: {type: number}}}
|
||||
""", (slicedDocument, _) =>
|
||||
{
|
||||
Assert.NotNull(slicedDocument);
|
||||
Assert.NotEmpty(slicedDocument.Paths);
|
||||
var schema = slicedDocument.Paths["/test"].Operations[OperationType.Post].RequestBody
|
||||
.Content["application/json"].Schema;
|
||||
Assert.Equal("object", schema.Type);
|
||||
Assert.Single(schema.Properties);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ValidationSchemaTestInput))]
|
||||
public async Task MergesAllOfRequestBodyAsync(string content, Action<OpenApiDocument, OpenApiDiagnostic> assertions)
|
||||
{
|
||||
var apiDescription = $"""
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: test
|
||||
version: "1.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
description: description for test path
|
||||
requestBody:
|
||||
required: true
|
||||
{content}
|
||||
responses:
|
||||
'200':
|
||||
description: "success"
|
||||
""";
|
||||
// creates a new schema with both type:string & maxLength:5
|
||||
var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml";
|
||||
await File.WriteAllTextAsync(simpleDescriptionPath, apiDescription);
|
||||
var mockLogger = new Mock<ILogger<PluginsGenerationService>>();
|
||||
var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object);
|
||||
var outputDirectory = Path.Combine(workingDirectory, "output");
|
||||
var generationConfiguration = new GenerationConfiguration
|
||||
{
|
||||
OutputPath = outputDirectory,
|
||||
OpenAPIFilePath = "openapiPath",
|
||||
PluginTypes = [PluginType.APIPlugin],
|
||||
ClientClassName = "client",
|
||||
ApiRootUrl = "http://localhost/", //Kiota builder would set this for us
|
||||
};
|
||||
var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false);
|
||||
var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration);
|
||||
KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument);
|
||||
var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel);
|
||||
|
||||
var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory);
|
||||
await pluginsGenerationService.GenerateManifestAsync();
|
||||
|
||||
Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName)));
|
||||
Assert.True(File.Exists(Path.Combine(outputDirectory, OpenApiFileName)));
|
||||
|
||||
try
|
||||
{
|
||||
// Validate the sliced openapi
|
||||
var slicedApiContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, OpenApiFileName));
|
||||
var r = new OpenApiStringReader();
|
||||
var slicedDocument = r.Read(slicedApiContent, out var diagnostic);
|
||||
assertions(slicedDocument, diagnostic);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(outputDirectory);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче