Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds support for OpenAPI 3.1 descriptions #5936

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
85194e4
fix: oas extensions parsing
baywet Oct 14, 2024
d83bc2f
fix: multiple NRE
baywet Oct 14, 2024
99623e1
fix: additional NRE
baywet Oct 14, 2024
8392023
chore: updates reference to oai.net preview
baywet Nov 12, 2024
25e4511
fix: additional 3.1 NRT and type reference fixes
baywet Nov 12, 2024
99dcbfb
fix: additional 3.1 NRT and type reference fixes
baywet Nov 12, 2024
6422b45
fix: wrong value comparison
baywet Nov 12, 2024
4bff227
fix: additional type and NRT fixes
baywet Nov 12, 2024
229a425
fix: main project builds
baywet Nov 12, 2024
193a7b9
fix: all validation tests migrated to the new OAI.net object model
baywet Nov 12, 2024
294d80e
fix: openapi extension migrated to new OAI.net model
baywet Nov 12, 2024
2d87938
fix: main tests build
baywet Nov 12, 2024
453bf9d
Merge branch 'main' into feat/OAI-3-1
baywet Nov 25, 2024
57c7cb9
fix: parsing of dependencies
baywet Nov 25, 2024
5db7ad0
fix: aligns dependencies versions
baywet Nov 25, 2024
2360204
Merge branch 'main' into feat/OAI-3-1
baywet Nov 27, 2024
4c1866d
chore: upgrades additional code
baywet Nov 27, 2024
291db8a
Merge branch 'main' into feat/OAI-3-1
baywet Dec 20, 2024
4daed72
chore: upgrades to OAI.net preview3
baywet Dec 20, 2024
9c9f988
fix: registers missing reader
baywet Dec 20, 2024
9cebd6c
fix: bad boolean algebra
baywet Dec 20, 2024
893a5ae
chore: linting
baywet Dec 20, 2024
c1e21aa
fix: class naming for discriminator mapping
baywet Dec 20, 2024
5f7e287
fix: further fix of missing reference ids
baywet Dec 20, 2024
77f3a55
fix: wrong method overload use
baywet Dec 20, 2024
66641be
fix: wrong test data
baywet Dec 20, 2024
52ede22
fix: missing readers
baywet Dec 20, 2024
85c67cd
fix: form encoded validation rule
baywet Dec 20, 2024
bc64b83
fix: validation rules registration
baywet Dec 20, 2024
21c607f
Merge branch 'main' into feat/OAI-3-1
baywet Dec 23, 2024
f5b95b7
fix: rules disablement did not work anymore
baywet Dec 23, 2024
c539330
fix: invalid payload for unit test
baywet Dec 23, 2024
c1cbea9
chore: improves validation tests performance for API export
baywet Dec 23, 2024
3cdd3d4
fix: document path for manifest generation tests
baywet Dec 23, 2024
99c4675
fix: invalid type for test data
baywet Dec 23, 2024
1886d77
chore: linting removes unnecessary string interpolation
baywet Dec 23, 2024
7f16e6b
fix: navigation properties type generation
baywet Dec 23, 2024
118e524
fix: binary and byte format mapping
baywet Dec 23, 2024
8782bd6
fix: enum values number parsing
baywet Dec 23, 2024
3caf91e
Merge branch 'main' into feat/OAI-3-1
baywet Dec 24, 2024
1ca8341
chore: bumps oai.net preview version
baywet Dec 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions src/Kiota.Builder/Configuration/LanguagesInformation.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Writers;

namespace Kiota.Builder.Configuration;

public class LanguagesInformation : Dictionary<string, LanguageInformation>, IOpenApiSerializable, ICloneable
{
public void SerializeAsV2(IOpenApiWriter writer) => SerializeAsV3(writer);
public void SerializeAsV3(IOpenApiWriter writer)
public void SerializeAsV2(IOpenApiWriter writer) => SerializeAsV31(writer);
public void SerializeAsV3(IOpenApiWriter writer) => SerializeAsV31(writer);
public static LanguagesInformation Parse(JsonObject jsonNode)
{
ArgumentNullException.ThrowIfNull(writer);
writer.WriteStartObject();
foreach (var entry in this.OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteRequiredObject(entry.Key, entry.Value, (w, x) => x.SerializeAsV3(w));
}
writer.WriteEndObject();
}
public static LanguagesInformation Parse(IOpenApiAny source)
{
if (source is not OpenApiObject rawObject) throw new ArgumentOutOfRangeException(nameof(source));
var extension = new LanguagesInformation();
foreach (var property in rawObject)
extension.Add(property.Key, LanguageInformation.Parse(property.Value));
foreach (var property in jsonNode.Where(static property => property.Value is JsonObject))
extension.Add(property.Key, LanguageInformation.Parse(property.Value!));

return extension;
}

Expand All @@ -36,4 +27,15 @@ public object Clone()
result.Add(entry.Key, entry.Value);// records don't need to be cloned as they are immutable
return result;
}

public void SerializeAsV31(IOpenApiWriter writer)
{
ArgumentNullException.ThrowIfNull(writer);
writer.WriteStartObject();
foreach (var entry in this.OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteRequiredObject(entry.Key, entry.Value, (w, x) => x.SerializeAsV3(w));
}
writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ internal static DeprecationInformation GetDeprecationInformation(this OpenApiPar
return deprecatedValue.ToDeprecationInformation();
else if (parameter.Schema != null && !parameter.Schema.IsReferencedSchema() && parameter.Schema.Deprecated)
return parameter.Schema.GetDeprecationInformation();
else if (parameter.Content.Values.Select(static x => x.Schema).Where(static x => x != null && !x.IsReferencedSchema() && x.Deprecated).Select(static x => x.GetDeprecationInformation()).FirstOrDefault(static x => x.IsDeprecated) is DeprecationInformation contentDeprecationInformation)
else if (parameter.Content.Values.Select(static x => x.Schema).Where(static x => x != null && !x.IsReferencedSchema() && x.Deprecated).Select(static x => x!.GetDeprecationInformation()).FirstOrDefault(static x => x.IsDeprecated) is DeprecationInformation contentDeprecationInformation)
return contentDeprecationInformation;
return new(null, null, null, null, parameter.Deprecated);
}
internal static DeprecationInformation GetDeprecationInformation(this OpenApiOperation operation)
{
if (operation.Deprecated && operation.Extensions.TryGetValue(OpenApiDeprecationExtension.Name, out var deprecatedExtension) && deprecatedExtension is OpenApiDeprecationExtension deprecatedValue)
if (operation.Deprecated && operation.Extensions is not null && operation.Extensions.TryGetValue(OpenApiDeprecationExtension.Name, out var deprecatedExtension) && deprecatedExtension is OpenApiDeprecationExtension deprecatedValue)
return deprecatedValue.ToDeprecationInformation();
else if (operation.Responses.Values
else if (operation.Responses?.Values
.SelectMany(static x => x.Content.Values)
.Select(static x => x?.Schema)
.OfType<OpenApiSchema>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal static void InitializeInheritanceIndex(this OpenApiDocument openApiDocu
{
ArgumentNullException.ThrowIfNull(openApiDocument);
var candidateUrl = openApiDocument.Servers
.GroupBy(static x => x, new OpenApiServerComparer()) //group by protocol relative urls
?.GroupBy(static x => x, new OpenApiServerComparer()) //group by protocol relative urls
.FirstOrDefault()
?.OrderByDescending(static x => x.Url, StringComparer.OrdinalIgnoreCase) // prefer https over http
?.FirstOrDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private static string vendorSpecificCleanup(string input)
}
internal static IEnumerable<OpenApiSchema> GetResponseSchemas(this OpenApiOperation operation, HashSet<string> successCodesToUse, StructuredMimeTypesCollection structuredMimeTypes)
{
if (operation.Responses is null) return [];
// Return Schema that represents all the possible success responses!
return operation.Responses.Where(r => successCodesToUse.Contains(r.Key))
.OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase)
Expand Down Expand Up @@ -67,7 +68,7 @@ internal static IEnumerable<OpenApiSchema> GetValidSchemas(this IDictionary<stri
.Select(static c => (Key: c.Key.Split(';', StringSplitOptions.RemoveEmptyEntries)[0], c.Value))
.Where(c => structuredMimeTypes.Contains(c.Key) || structuredMimeTypes.Contains(vendorSpecificCleanup(c.Key)))
.Select(static co => co.Value.Schema)
.Where(static s => s is not null);
.OfType<OpenApiSchema>();
}
internal static OpenApiSchema? GetResponseSchema(this OpenApiResponse response, StructuredMimeTypesCollection structuredMimeTypes)
{
Expand Down
53 changes: 28 additions & 25 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using System.Text.Json;
using Microsoft.OpenApi.Models;

namespace Kiota.Builder.Extensions;
Expand Down Expand Up @@ -53,7 +53,7 @@ public static bool IsReferencedSchema(this OpenApiSchema schema)

public static bool IsArray(this OpenApiSchema? schema)
{
return "array".Equals(schema?.Type, StringComparison.OrdinalIgnoreCase) && schema.Items != null &&
return schema is { Type: JsonSchemaType.Array or (JsonSchemaType.Array | JsonSchemaType.Null) } && schema.Items is not null &&
(schema.Items.IsComposedEnum() ||
schema.Items.IsEnum() ||
schema.Items.IsSemanticallyMeaningful() ||
Expand All @@ -62,7 +62,7 @@ public static bool IsArray(this OpenApiSchema? schema)

public static bool IsObjectType(this OpenApiSchema? schema)
{
return "object".Equals(schema?.Type, StringComparison.OrdinalIgnoreCase);
return schema is { Type: JsonSchemaType.Object or (JsonSchemaType.Object | JsonSchemaType.Null) };
}
public static bool HasAnyProperty(this OpenApiSchema? schema)
{
Expand All @@ -80,7 +80,7 @@ public static bool IsInherited(this OpenApiSchema? schema)
var meaningfulMemberSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)
.Where(static x => x.IsSemanticallyMeaningful(ignoreEnums: true, ignoreArrays: true, ignoreType: true))
// the next line ensures the meaningful schema are objects as it won't make sense inheriting from a primitive despite it being meaningful.
.Where(static x => string.IsNullOrEmpty(x.Reference?.Id) || string.IsNullOrEmpty(x.Type) || "object".Equals(x.Type, StringComparison.OrdinalIgnoreCase))
.Where(static x => string.IsNullOrEmpty(x.Reference?.Id) || x.Type is null || !x.Type.HasValue || (x.Type.Value & JsonSchemaType.Object) is JsonSchemaType.Object)
.ToArray();
var isRootSchemaMeaningful = schema.IsSemanticallyMeaningful(ignoreEnums: true, ignoreArrays: true, ignoreType: true);
return meaningfulMemberSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 &&
Expand Down Expand Up @@ -162,45 +162,48 @@ public static bool IsExclusiveUnion(this OpenApiSchema? schema, uint exclusiveMi
return schema?.OneOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > exclusiveMinimumNumberOfEntries;
// so we don't consider one of object/nullable as a union type
}
private static readonly HashSet<string> oDataTypes = new(StringComparer.OrdinalIgnoreCase) {
"number",
"integer",
};
private static readonly HashSet<JsonSchemaType> oDataTypes = [
JsonSchemaType.Number,
JsonSchemaType.Integer,
];
private static readonly Func<OpenApiSchema, bool> isODataType = static x => x.Type is not null && oDataTypes.Contains(x.Type.Value);
private static readonly Func<OpenApiSchema, bool> isStringType = static x => x is { Type: JsonSchemaType.String or (JsonSchemaType.String | JsonSchemaType.Null) };
private static bool IsODataPrimitiveTypeBackwardCompatible(this OpenApiSchema schema)
{
return schema.IsExclusiveUnion() &&
schema.OneOf.Count == 3 &&
schema.OneOf.Count(static x => x.Enum?.Any() ?? false) == 1 &&
schema.OneOf.Count(static x => oDataTypes.Contains(x.Type)) == 1 &&
schema.OneOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase)) == 1
schema.OneOf.Count(isODataType) == 1 &&
schema.OneOf.Count(isStringType) == 1
||
schema.IsInclusiveUnion() &&
schema.AnyOf.Count == 3 &&
schema.AnyOf.Count(static x => x.Enum?.Any() ?? false) == 1 &&
schema.AnyOf.Count(static x => oDataTypes.Contains(x.Type)) == 1 &&
schema.AnyOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase)) == 1;
schema.AnyOf.Count(isODataType) == 1 &&
schema.AnyOf.Count(isStringType) == 1;
}
public static bool IsODataPrimitiveType(this OpenApiSchema schema)
{
return schema.IsExclusiveUnion() &&
schema.OneOf.Count == 3 &&
schema.OneOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase) && (x.Enum?.Any() ?? false)) == 1 &&
schema.OneOf.Count(static x => oDataTypes.Contains(x.Type)) == 1 &&
schema.OneOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase)) == 2
schema.OneOf.Count(static x => isStringType(x) && (x.Enum?.Any() ?? false)) == 1 &&
schema.OneOf.Count(isODataType) == 1 &&
schema.OneOf.Count(isStringType) == 2
||
schema.IsInclusiveUnion() &&
schema.AnyOf.Count == 3 &&
schema.AnyOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase) && (x.Enum?.Any() ?? false)) == 1 &&
schema.AnyOf.Count(static x => oDataTypes.Contains(x.Type)) == 1 &&
schema.AnyOf.Count(static x => "string".Equals(x.Type, StringComparison.OrdinalIgnoreCase)) == 2
schema.AnyOf.Count(static x => isStringType(x) && (x.Enum?.Any() ?? false)) == 1 &&
schema.AnyOf.Count(isODataType) == 1 &&
schema.AnyOf.Count(isStringType) == 2
||
schema.IsODataPrimitiveTypeBackwardCompatible();
}
public static bool IsEnum(this OpenApiSchema schema)
{
if (schema is null) return false;
return schema.Enum.OfType<OpenApiString>().Any(static x => !string.IsNullOrEmpty(x.Value)) &&
(string.IsNullOrEmpty(schema.Type) || "string".Equals(schema.Type, StringComparison.OrdinalIgnoreCase)); // number and boolean enums are not supported
return schema.Enum.Any(static x => x.GetValueKind() is JsonValueKind.String &&
x.GetValue<string>() is string value &&
!string.IsNullOrEmpty(value)); // number and boolean enums are not supported
}
public static bool IsComposedEnum(this OpenApiSchema schema)
{
Expand All @@ -214,8 +217,8 @@ public static bool IsSemanticallyMeaningful(this OpenApiSchema schema, bool igno
return schema.HasAnyProperty() ||
(!ignoreEnums && schema.Enum is { Count: > 0 }) ||
(!ignoreArrays && schema.Items != null) ||
(!ignoreType && !string.IsNullOrEmpty(schema.Type) &&
((ignoreNullableObjects && !"object".Equals(schema.Type, StringComparison.OrdinalIgnoreCase)) ||
(!ignoreType && schema.Type is not null &&
((ignoreNullableObjects && !schema.IsObjectType()) ||
!ignoreNullableObjects)) ||
!string.IsNullOrEmpty(schema.Format) ||
!string.IsNullOrEmpty(schema.Reference?.Id);
Expand Down Expand Up @@ -306,7 +309,7 @@ internal static string GetDiscriminatorPropertyName(this OpenApiSchema schema, H
internal static IEnumerable<KeyValuePair<string, string>> GetDiscriminatorMappings(this OpenApiSchema schema, ConcurrentDictionary<string, ConcurrentDictionary<string, bool>> inheritanceIndex)
{
if (schema == null)
return Enumerable.Empty<KeyValuePair<string, string>>();
return [];
if (!(schema.Discriminator?.Mapping?.Any() ?? false))
if (schema.OneOf.Any())
return schema.OneOf.SelectMany(x => GetDiscriminatorMappings(x, inheritanceIndex));
Expand All @@ -319,9 +322,9 @@ internal static IEnumerable<KeyValuePair<string, string>> GetDiscriminatorMappin
return GetAllInheritanceSchemaReferences(schema.Reference.Id, inheritanceIndex)
.Where(static x => !string.IsNullOrEmpty(x))
.Select(x => KeyValuePair.Create(x, x))
.Union(new[] { KeyValuePair.Create(schema.Reference.Id, schema.Reference.Id) });
.Union([KeyValuePair.Create(schema.Reference.Id, schema.Reference.Id)]);
else
return Enumerable.Empty<KeyValuePair<string, string>>();
return [];

return schema.Discriminator
.Mapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Kiota.Builder.OpenApiExtensions;
using Microsoft.OpenApi.Readers;
using Microsoft.OpenApi.Reader;

namespace Kiota.Builder.Extensions;
public static class OpenApiSettingsExtensions
Expand Down
16 changes: 8 additions & 8 deletions src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ internal static string CleanupParametersFromPath(string pathSegment)
private static IEnumerable<OpenApiParameter> GetParametersForPathItem(OpenApiPathItem pathItem, string nodeSegment)
{
return pathItem.Parameters
.Union(pathItem.Operations.SelectMany(static x => x.Value.Parameters))
.Union(pathItem.Operations.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>()))
.Where(static x => x.In == ParameterLocation.Path)
.Where(x => nodeSegment.Contains($"{{{x.Name}}}", StringComparison.OrdinalIgnoreCase));
}
Expand Down Expand Up @@ -193,7 +193,7 @@ public static bool HasRequiredQueryParametersAcrossOperations(this OpenApiUrlTre
if (!currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem))
return false;

var operationQueryParameters = pathItem.Operations.SelectMany(static x => x.Value.Parameters);
var operationQueryParameters = pathItem.Operations.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>());
return operationQueryParameters.Union(pathItem.Parameters).Where(static x => x.In == ParameterLocation.Query)
.Any(static x => x.Required);
}
Expand All @@ -207,9 +207,9 @@ public static string GetUrlTemplate(this OpenApiUrlTreeNode currentNode, Operati
var pathItem = currentNode.PathItems[Constants.DefaultOpenApiLabel];
var operationQueryParameters = (operationType, pathItem.Operations.Any()) switch
{
(OperationType ot, _) when pathItem.Operations.TryGetValue(ot, out var operation) => operation.Parameters,
(null, true) => pathItem.Operations.SelectMany(static x => x.Value.Parameters).Where(static x => x.In == ParameterLocation.Query),
_ => Enumerable.Empty<OpenApiParameter>(),
(OperationType ot, _) when pathItem.Operations.TryGetValue(ot, out var operation) && operation.Parameters is not null => operation.Parameters,
(null, true) => pathItem.Operations.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>()).Where(static x => x.In == ParameterLocation.Query),
_ => [],
};
var parameters = pathItem.Parameters
.Union(operationQueryParameters)
Expand All @@ -234,7 +234,7 @@ public static string GetUrlTemplate(this OpenApiUrlTreeNode currentNode, Operati
}
var pathReservedPathParametersIds = currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pItem) ?
pItem.Parameters
.Union(pItem.Operations.SelectMany(static x => x.Value.Parameters))
.Union(pItem.Operations.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>()))
.Where(static x => x.In == ParameterLocation.Path && x.Extensions.TryGetValue(OpenApiReservedParameterExtension.Name, out var ext) && ext is OpenApiReservedParameterExtension reserved && reserved.IsReserved.HasValue && reserved.IsReserved.Value)
.Select(static x => x.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase) :
Expand Down Expand Up @@ -356,7 +356,7 @@ private static void ReplaceParameterInPathForAllChildNodes(OpenApiUrlTreeNode no
if (node.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem))
{
foreach (var pathParameter in pathItem.Parameters
.Union(pathItem.Operations.SelectMany(static x => x.Value.Parameters))
.Union(pathItem.Operations.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>()))
.Where(x => x.In == ParameterLocation.Path && oldName.Equals(x.Name, StringComparison.Ordinal)))
{
pathParameter.Name = newParameterName;
Expand Down Expand Up @@ -386,7 +386,7 @@ private static void CopyNodeIntoOtherNode(OpenApiUrlTreeNode source, OpenApiUrlT
pathItem
.Value
.Operations
.SelectMany(static x => x.Value.Parameters)
.SelectMany(static x => x.Value.Parameters ?? Enumerable.Empty<OpenApiParameter>())
.Where(x => x.In == ParameterLocation.Path && pathParameterNameToReplace.Equals(x.Name, StringComparison.Ordinal))
))
{
Expand Down
Loading