diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index 49681cdb..8f69cefa 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using NJsonSchema; using NJsonSchema.CodeGeneration; using NJsonSchema.CodeGeneration.CSharp; @@ -7,21 +6,36 @@ namespace Refitter.Core; -internal class CSharpClientGeneratorFactory(RefitGeneratorSettings settings, OpenApiDocument document) +internal class CSharpClientGeneratorFactory { - public CustomCSharpClientGenerator Create() + private readonly RefitGeneratorSettings settings; + private readonly OpenApiDocument document; + private readonly IReadOnlyList mutators; + + public CSharpClientGeneratorFactory( + RefitGeneratorSettings settings, + OpenApiDocument document, + IReadOnlyList? mutators = null) { - if (!settings.GenerateDefaultAdditionalProperties && document.Components?.Schemas != null) - { - foreach (var kvp in document.Components.Schemas) - { - kvp.Value.ActualSchema.AllowAdditionalProperties = false; - } - } + this.settings = settings; + this.document = document; + this.mutators = mutators ?? CreateDefaultMutators(settings); + } - ConvertOneOfWithDiscriminatorToAllOf(); - FixMissingTypesWithIntegerFormat(); - ApplyCustomIntegerType(); + private static IReadOnlyList CreateDefaultMutators( + RefitGeneratorSettings settings) => + [ + new DisableAdditionalPropertiesMutator(settings.GenerateDefaultAdditionalProperties), + new OneOfDiscriminatorToAllOfMutator(), + new FixMissingIntegerTypesMutator(), + new CustomIntegerTypeMutator( + settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32), + ]; + + public CustomCSharpClientGenerator Create() + { + foreach (var mutator in mutators) + mutator.Mutate(document); var csharpClientGeneratorSettings = new CSharpClientGeneratorSettings { @@ -67,7 +81,7 @@ public CustomCSharpClientGenerator Create() document, csharpClientGeneratorSettings); - MapCSharpGeneratorSettings( + ApplyCodeGeneratorSettings( settings.CodeGeneratorSettings, generator.Settings.CSharpGeneratorSettings); @@ -121,247 +135,7 @@ private IEnumerable GetNamedSchemaHints() } } - /// - /// Converts schemas that use oneOf/anyOf with a discriminator to use the allOf inheritance - /// pattern that NSwag's C# code generator understands. Without this transformation, - /// NSwag generates undefined anonymous types (e.g., "IdentityProvider2") instead of - /// proper base class references. - /// - private void ConvertOneOfWithDiscriminatorToAllOf() - { - // Null-safe check for Swagger 2.0 docs that use definitions instead (#1015) - if (document.Components?.Schemas == null) - return; - - foreach (var kvp in document.Components.Schemas) - { - var schema = kvp.Value?.ActualSchema; - if (schema == null) - continue; - - if (schema.DiscriminatorObject == null) - continue; - - var unionSchemas = schema.OneOf.Concat(schema.AnyOf).ToArray(); - if (unionSchemas.Length == 0) - continue; - - // Ensure the base schema is typed as an object - if (schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) - schema.Type = JsonObjectType.Object; - - // For each subtype, add allOf pointing to the base schema if not already present - foreach (var subSchemaRef in unionSchemas) - { - var subSchema = subSchemaRef?.ActualSchema; - if (subSchema == null) - continue; - - bool alreadyInherits = subSchema.AllOf.Any( - a => a.HasReference && a.ActualSchema == schema); - if (!alreadyInherits) - { - var reference = new JsonSchema { Reference = schema }; - subSchema.AllOf.Add(reference); - } - } - - // Remove the oneOf/anyOf from the base schema now that subtypes use allOf - schema.OneOf.Clear(); - schema.AnyOf.Clear(); - } - } - - private void FixMissingTypesWithIntegerFormat() => - TraverseDocumentSchemas(FixSchemaTypeFromFormat); - - private void FixSchemaTypeFromFormat(JsonSchema schema) - { - // If type is not set but format indicates a numeric type (int32, int64, float, double), set the type based on format - if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && - !string.IsNullOrEmpty(schema.Format)) - { - if (schema.Format == "int32" || schema.Format == "int64") - { - schema.Type = JsonObjectType.Integer; - } - else if (schema.Format == "float" || schema.Format == "double") - { - schema.Type = JsonObjectType.Number; - } - } - } - - private void ApplyCustomIntegerType() - { - var customIntegerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32; - if (customIntegerType == IntegerType.Int32) - return; - - TraverseDocumentSchemas(FixSchemaIntegerFormat); - } - - private static void FixSchemaIntegerFormat(JsonSchema schema) - { - if (schema.Type == JsonObjectType.Integer && - string.IsNullOrEmpty(schema.Format)) - { - schema.Format = "int64"; - } - } - - private void TraverseDocumentSchemas(Action visitor) - { - var visited = new HashSet(JsonSchemaReferenceComparer.Instance); - var schemasToProcess = new Stack(); - - foreach (var schema in EnumerateDocumentSchemaRoots()) - { - TryPush(schema, schemasToProcess); - } - - while (schemasToProcess.Count > 0) - { - var actualSchema = schemasToProcess.Pop().ActualSchema; - if (!visited.Add(actualSchema)) - { - continue; - } - - visitor(actualSchema); - - foreach (var childSchema in EnumerateTraversableSchemas(actualSchema)) - { - TryPush(childSchema, schemasToProcess); - } - } - } - - private IEnumerable EnumerateDocumentSchemaRoots() - { - if (document.Components?.Schemas != null) - { - foreach (var schema in document.Components.Schemas.Values) - { - yield return schema; - } - } - - if (document.Paths == null) - { - yield break; - } - - foreach (var pathItem in document.Paths.Values) - { - if (pathItem == null) - { - continue; - } - - foreach (var parameter in pathItem.Parameters) - { - yield return parameter; - } - - foreach (var operation in pathItem.Values) - { - if (operation == null) - { - continue; - } - - foreach (var parameter in operation.ActualParameters) - { - yield return parameter; - } - - if (operation.RequestBody?.Content != null) - { - foreach (var content in operation.RequestBody.Content.Values) - { - yield return content.Schema; - } - } - - foreach (var response in operation.ActualResponses.Values) - { - if (response.Headers != null) - { - foreach (var header in response.Headers.Values) - { - yield return header; - } - } - - if (response.Content == null) - { - continue; - } - - foreach (var content in response.Content.Values) - { - yield return content.Schema; - } - } - } - } - } - - private static IEnumerable EnumerateTraversableSchemas(JsonSchema schema) - { - yield return schema.AdditionalItemsSchema; - yield return schema.AdditionalPropertiesSchema; - yield return schema.DictionaryKey; - yield return schema.Item; - - if (schema.Items.Count != 0) - { - foreach (var item in schema.Items) - { - yield return item; - } - } - - yield return schema.Not; - - foreach (var property in schema.Properties.Values) - { - yield return property; - } - - foreach (var subSchema in schema.AllOf) - { - yield return subSchema; - } - - foreach (var subSchema in schema.OneOf) - { - yield return subSchema; - } - - foreach (var subSchema in schema.AnyOf) - { - yield return subSchema; - } - - foreach (var definition in schema.Definitions.Values) - { - yield return definition; - } - } - - private static void TryPush(JsonSchema? schema, Stack stack) - { - if (schema == null) - { - return; - } - - stack.Push(schema); - } - - private static void MapCSharpGeneratorSettings( + private static void ApplyCodeGeneratorSettings( CodeGeneratorSettings? source, CSharpGeneratorSettings destination) { @@ -370,103 +144,40 @@ private static void MapCSharpGeneratorSettings( return; } - var defaultInstance = new CodeGeneratorSettings(); - foreach (var property in source.GetType().GetProperties()) - { - var value = property.GetValue(source); - if (value == null) - { - continue; - } - - if (value.Equals(property.GetValue(defaultInstance))) - { - continue; - } - - var settingsProperty = destination.GetType().GetProperty(property.Name); - if (settingsProperty == null || - !settingsProperty.PropertyType.IsAssignableFrom(property.PropertyType)) - { - continue; - } - - settingsProperty.SetValue(destination, value); - } - } - - /// - /// custom template factory - /// solely for the purpose of tweaking the JsonPolymorphic attribute with UnknownDerivedTypeHandling = FallBackToBaseType and IgnoreUnrecognizedTypeDiscriminators = true - /// This class should be removed if NSwag eventually supports setting UnknownDerivedTypeHandling and IgnoreUnrecognizedTypeDiscriminators. - /// - private class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory - { - /// Initializes a new instance of the class. - /// The settings. - public CustomTemplateFactory(CodeGeneratorSettingsBase settings) - : base(settings, [typeof(CSharpGenerator).Assembly, typeof(CSharpGeneratorBaseSettings).Assembly, typeof(NJsonSchema.CodeGeneration.CSharp.CSharpGeneratorSettings).Assembly]) - { + destination.RequiredPropertiesMustBeDefined = source.RequiredPropertiesMustBeDefined; + destination.GenerateDataAnnotations = source.GenerateDataAnnotations; + destination.AnyType = source.AnyType; + destination.DateType = source.DateType; + destination.DateTimeType = source.DateTimeType; + destination.TimeType = source.TimeType; + destination.TimeSpanType = source.TimeSpanType; + destination.ArrayType = source.ArrayType; + destination.DictionaryType = source.DictionaryType; + destination.ArrayInstanceType = source.ArrayInstanceType; + destination.DictionaryInstanceType = source.DictionaryInstanceType; + destination.ArrayBaseType = source.ArrayBaseType; + destination.DictionaryBaseType = source.DictionaryBaseType; + destination.PropertySetterAccessModifier = source.PropertySetterAccessModifier; + destination.JsonConverters = source.JsonConverters; + destination.GenerateImmutableArrayProperties = source.GenerateImmutableArrayProperties; + destination.GenerateImmutableDictionaryProperties = source.GenerateImmutableDictionaryProperties; + destination.HandleReferences = source.HandleReferences; + destination.JsonSerializerSettingsTransformationMethod = source.JsonSerializerSettingsTransformationMethod; + destination.GenerateJsonMethods = source.GenerateJsonMethods; + destination.EnforceFlagEnums = source.EnforceFlagEnums; + destination.InlineNamedDictionaries = source.InlineNamedDictionaries; + destination.InlineNamedTuples = source.InlineNamedTuples; + destination.InlineNamedArrays = source.InlineNamedArrays; + destination.GenerateOptionalPropertiesAsNullable = source.GenerateOptionalPropertiesAsNullable; + destination.GenerateNullableReferenceTypes = source.GenerateNullableReferenceTypes; + destination.GenerateNativeRecords = source.GenerateNativeRecords; + destination.GenerateDefaultValues = source.GenerateDefaultValues; + destination.InlineNamedAny = source.InlineNamedAny; + destination.ExcludedTypeNames = source.ExcludedTypeNames; + + if (source.PropertyNameGenerator != null) + { + destination.PropertyNameGenerator = source.PropertyNameGenerator; } - - /// - protected override string GetEmbeddedLiquidTemplate(string language, string template) - { - var templateText = base.GetEmbeddedLiquidTemplate(language, template); - return template switch - { - "Class" => templateText - .Replace( - "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]", - "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]"), - _ => templateText, - }; - } - } -} - -internal sealed class JsonSchemaReferenceComparer : IEqualityComparer -{ - public static JsonSchemaReferenceComparer Instance { get; } = new(); - - public bool Equals(JsonSchema? x, JsonSchema? y) => - ReferenceEquals(x, y); - - public int GetHashCode(JsonSchema obj) => - RuntimeHelpers.GetHashCode(obj); -} - -internal sealed class SafeSchemaTypeNameGenerator( - HashSet preferredExactTypeNameHints) : ITypeNameGenerator -{ - private const string AnonymousTypeName = "Anonymous"; - private readonly DefaultTypeNameGenerator inner = new(); - - public string Generate( - JsonSchema schema, - string? typeNameHint, - IEnumerable reservedTypeNames) - { - var normalizedHint = IdentifierUtils.NormalizeSchemaTypeNameHint(typeNameHint) - ?? IdentifierUtils.NormalizeSchemaTypeNameHint(schema.Title) - ?? AnonymousTypeName; - - var typeNames = reservedTypeNames as string[] ?? reservedTypeNames.ToArray(); - if (!string.IsNullOrEmpty(typeNameHint) && - !string.Equals(typeNameHint, normalizedHint, StringComparison.Ordinal) && - preferredExactTypeNameHints.Contains(normalizedHint)) - { - var reservedHints = typeNames - .Concat(preferredExactTypeNameHints); - - var reservedHintSet = new HashSet(reservedHints, StringComparer.Ordinal); - - normalizedHint = IdentifierUtils.Counted(reservedHintSet, normalizedHint); - } - - var generatedTypeName = inner.Generate(schema, normalizedHint, typeNames); - return string.IsNullOrWhiteSpace(generatedTypeName) - ? inner.Generate(schema, AnonymousTypeName, typeNames) - : generatedTypeName; } } diff --git a/src/Refitter.Core/CustomIntegerTypeMutator.cs b/src/Refitter.Core/CustomIntegerTypeMutator.cs new file mode 100644 index 00000000..694be3cf --- /dev/null +++ b/src/Refitter.Core/CustomIntegerTypeMutator.cs @@ -0,0 +1,25 @@ +using NJsonSchema; +using NSwag; + +namespace Refitter.Core; + +internal sealed class CustomIntegerTypeMutator(IntegerType customIntegerType) + : IOpenApiDocumentMutator +{ + public void Mutate(OpenApiDocument document) + { + if (customIntegerType == IntegerType.Int32) + return; + + SchemaWalker.TraverseDocumentSchemas(document, FixSchemaIntegerFormat); + } + + private static void FixSchemaIntegerFormat(JsonSchema schema) + { + if (schema.Type == JsonObjectType.Integer && + string.IsNullOrEmpty(schema.Format)) + { + schema.Format = "int64"; + } + } +} diff --git a/src/Refitter.Core/CustomTemplateFactory.cs b/src/Refitter.Core/CustomTemplateFactory.cs new file mode 100644 index 00000000..049388f7 --- /dev/null +++ b/src/Refitter.Core/CustomTemplateFactory.cs @@ -0,0 +1,34 @@ +using NJsonSchema; +using NJsonSchema.CodeGeneration; +using NJsonSchema.CodeGeneration.CSharp; +using NSwag; +using NSwag.CodeGeneration.CSharp; + +namespace Refitter.Core; + +/// +/// Custom template factory solely for the purpose of tweaking the JsonPolymorphic attribute +/// with UnknownDerivedTypeHandling = FallBackToBaseType and IgnoreUnrecognizedTypeDiscriminators = true. +/// This class should be removed if NSwag eventually supports setting UnknownDerivedTypeHandling +/// and IgnoreUnrecognizedTypeDiscriminators. +/// +internal class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory +{ + public CustomTemplateFactory(CodeGeneratorSettingsBase settings) + : base(settings, [typeof(CSharpGenerator).Assembly, typeof(CSharpGeneratorBaseSettings).Assembly, typeof(NJsonSchema.CodeGeneration.CSharp.CSharpGeneratorSettings).Assembly]) + { + } + + protected override string GetEmbeddedLiquidTemplate(string language, string template) + { + var templateText = base.GetEmbeddedLiquidTemplate(language, template); + return template switch + { + "Class" => templateText + .Replace( + "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]", + "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]"), + _ => templateText, + }; + } +} diff --git a/src/Refitter.Core/DisableAdditionalPropertiesMutator.cs b/src/Refitter.Core/DisableAdditionalPropertiesMutator.cs new file mode 100644 index 00000000..6dd8ca55 --- /dev/null +++ b/src/Refitter.Core/DisableAdditionalPropertiesMutator.cs @@ -0,0 +1,21 @@ +using NSwag; + +namespace Refitter.Core; + +internal sealed class DisableAdditionalPropertiesMutator(bool generateDefaultAdditionalProperties) + : IOpenApiDocumentMutator +{ + public void Mutate(OpenApiDocument document) + { + if (generateDefaultAdditionalProperties) + return; + + if (document.Components?.Schemas == null) + return; + + foreach (var kvp in document.Components.Schemas) + { + kvp.Value.ActualSchema.AllowAdditionalProperties = false; + } + } +} diff --git a/src/Refitter.Core/FixMissingIntegerTypesMutator.cs b/src/Refitter.Core/FixMissingIntegerTypesMutator.cs new file mode 100644 index 00000000..abe59b78 --- /dev/null +++ b/src/Refitter.Core/FixMissingIntegerTypesMutator.cs @@ -0,0 +1,28 @@ +using NJsonSchema; +using NSwag; + +namespace Refitter.Core; + +internal sealed class FixMissingIntegerTypesMutator : IOpenApiDocumentMutator +{ + public void Mutate(OpenApiDocument document) + { + SchemaWalker.TraverseDocumentSchemas(document, FixSchemaTypeFromFormat); + } + + private static void FixSchemaTypeFromFormat(JsonSchema schema) + { + if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && + !string.IsNullOrEmpty(schema.Format)) + { + if (schema.Format == "int32" || schema.Format == "int64") + { + schema.Type = JsonObjectType.Integer; + } + else if (schema.Format == "float" || schema.Format == "double") + { + schema.Type = JsonObjectType.Number; + } + } + } +} diff --git a/src/Refitter.Core/IOpenApiDocumentMutator.cs b/src/Refitter.Core/IOpenApiDocumentMutator.cs new file mode 100644 index 00000000..9d82baa1 --- /dev/null +++ b/src/Refitter.Core/IOpenApiDocumentMutator.cs @@ -0,0 +1,8 @@ +using NSwag; + +namespace Refitter.Core; + +internal interface IOpenApiDocumentMutator +{ + void Mutate(OpenApiDocument document); +} diff --git a/src/Refitter.Core/JsonSchemaReferenceComparer.cs b/src/Refitter.Core/JsonSchemaReferenceComparer.cs new file mode 100644 index 00000000..6b60a1ed --- /dev/null +++ b/src/Refitter.Core/JsonSchemaReferenceComparer.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; +using NJsonSchema; + +namespace Refitter.Core; + +internal sealed class JsonSchemaReferenceComparer : IEqualityComparer +{ + public static JsonSchemaReferenceComparer Instance { get; } = new(); + + public bool Equals(JsonSchema? x, JsonSchema? y) => + ReferenceEquals(x, y); + + public int GetHashCode(JsonSchema obj) => + RuntimeHelpers.GetHashCode(obj); +} diff --git a/src/Refitter.Core/OneOfDiscriminatorToAllOfMutator.cs b/src/Refitter.Core/OneOfDiscriminatorToAllOfMutator.cs new file mode 100644 index 00000000..bc283e9c --- /dev/null +++ b/src/Refitter.Core/OneOfDiscriminatorToAllOfMutator.cs @@ -0,0 +1,48 @@ +using NJsonSchema; +using NSwag; + +namespace Refitter.Core; + +internal sealed class OneOfDiscriminatorToAllOfMutator : IOpenApiDocumentMutator +{ + public void Mutate(OpenApiDocument document) + { + if (document.Components?.Schemas == null) + return; + + foreach (var kvp in document.Components.Schemas) + { + var schema = kvp.Value?.ActualSchema; + if (schema == null) + continue; + + if (schema.DiscriminatorObject == null) + continue; + + var unionSchemas = schema.OneOf.Concat(schema.AnyOf).ToArray(); + if (unionSchemas.Length == 0) + continue; + + if (schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) + schema.Type = JsonObjectType.Object; + + foreach (var subSchemaRef in unionSchemas) + { + var subSchema = subSchemaRef?.ActualSchema; + if (subSchema == null) + continue; + + bool alreadyInherits = subSchema.AllOf.Any( + a => a.HasReference && a.ActualSchema == schema); + if (!alreadyInherits) + { + var reference = new JsonSchema { Reference = schema }; + subSchema.AllOf.Add(reference); + } + } + + schema.OneOf.Clear(); + schema.AnyOf.Clear(); + } + } +} diff --git a/src/Refitter.Core/SafeSchemaTypeNameGenerator.cs b/src/Refitter.Core/SafeSchemaTypeNameGenerator.cs new file mode 100644 index 00000000..7807c7f8 --- /dev/null +++ b/src/Refitter.Core/SafeSchemaTypeNameGenerator.cs @@ -0,0 +1,39 @@ +using NJsonSchema; +using NJsonSchema.CodeGeneration; + +namespace Refitter.Core; + +internal sealed class SafeSchemaTypeNameGenerator( + HashSet preferredExactTypeNameHints) : ITypeNameGenerator +{ + private const string AnonymousTypeName = "Anonymous"; + private readonly DefaultTypeNameGenerator inner = new(); + + public string Generate( + JsonSchema schema, + string? typeNameHint, + IEnumerable reservedTypeNames) + { + var normalizedHint = IdentifierUtils.NormalizeSchemaTypeNameHint(typeNameHint) + ?? IdentifierUtils.NormalizeSchemaTypeNameHint(schema.Title) + ?? AnonymousTypeName; + + var typeNames = reservedTypeNames as string[] ?? reservedTypeNames.ToArray(); + if (!string.IsNullOrEmpty(typeNameHint) && + !string.Equals(typeNameHint, normalizedHint, StringComparison.Ordinal) && + preferredExactTypeNameHints.Contains(normalizedHint)) + { + var reservedHints = typeNames + .Concat(preferredExactTypeNameHints); + + var reservedHintSet = new HashSet(reservedHints, StringComparer.Ordinal); + + normalizedHint = IdentifierUtils.Counted(reservedHintSet, normalizedHint); + } + + var generatedTypeName = inner.Generate(schema, normalizedHint, typeNames); + return string.IsNullOrWhiteSpace(generatedTypeName) + ? inner.Generate(schema, AnonymousTypeName, typeNames) + : generatedTypeName; + } +} diff --git a/src/Refitter.Core/SchemaWalker.cs b/src/Refitter.Core/SchemaWalker.cs new file mode 100644 index 00000000..f65380bd --- /dev/null +++ b/src/Refitter.Core/SchemaWalker.cs @@ -0,0 +1,161 @@ +using NJsonSchema; +using NSwag; + +namespace Refitter.Core; + +internal static class SchemaWalker +{ + public static void TraverseDocumentSchemas( + OpenApiDocument document, + Action visitor) + { + var visited = new HashSet(JsonSchemaReferenceComparer.Instance); + var schemasToProcess = new Stack(); + + foreach (var schema in EnumerateDocumentSchemaRoots(document)) + { + TryPush(schema, schemasToProcess); + } + + while (schemasToProcess.Count > 0) + { + var actualSchema = schemasToProcess.Pop().ActualSchema; + if (!visited.Add(actualSchema)) + { + continue; + } + + visitor(actualSchema); + + foreach (var childSchema in EnumerateTraversableSchemas(actualSchema)) + { + TryPush(childSchema, schemasToProcess); + } + } + } + + public static IEnumerable EnumerateDocumentSchemaRoots( + OpenApiDocument document) + { + if (document.Components?.Schemas != null) + { + foreach (var schema in document.Components.Schemas.Values) + { + yield return schema; + } + } + + if (document.Paths == null) + { + yield break; + } + + foreach (var pathItem in document.Paths.Values) + { + if (pathItem == null) + { + continue; + } + + foreach (var parameter in pathItem.Parameters) + { + yield return parameter; + } + + foreach (var operation in pathItem.Values) + { + if (operation == null) + { + continue; + } + + foreach (var parameter in operation.ActualParameters) + { + yield return parameter; + } + + if (operation.RequestBody?.Content != null) + { + foreach (var content in operation.RequestBody.Content.Values) + { + yield return content.Schema; + } + } + + foreach (var response in operation.ActualResponses.Values) + { + if (response.Headers != null) + { + foreach (var header in response.Headers.Values) + { + yield return header; + } + } + + if (response.Content == null) + { + continue; + } + + foreach (var content in response.Content.Values) + { + yield return content.Schema; + } + } + } + } + } + + public static IEnumerable EnumerateTraversableSchemas(JsonSchema schema) + { + yield return schema.AdditionalItemsSchema; + yield return schema.AdditionalPropertiesSchema; + yield return schema.DictionaryKey; + yield return schema.Item; + + if (schema.Items.Count != 0) + { + foreach (var item in schema.Items) + { + yield return item; + } + } + + yield return schema.Not; + + foreach (var property in schema.Properties.Values) + { + yield return property; + } + + foreach (var subSchema in schema.AllOf) + { + yield return subSchema; + } + + foreach (var subSchema in schema.OneOf) + { + yield return subSchema; + } + + foreach (var subSchema in schema.AnyOf) + { + yield return subSchema; + } + + foreach (var definition in schema.Definitions.Values) + { + yield return definition; + } + } + + private static void TryPush(JsonSchema? schema, Stack stack) + { + if (schema == null) + { + return; + } + + stack.Push(schema); + } +} diff --git a/src/Refitter.Tests/CSharpClientGeneratorFactoryIntegrationTests.cs b/src/Refitter.Tests/CSharpClientGeneratorFactoryIntegrationTests.cs new file mode 100644 index 00000000..eaf516b2 --- /dev/null +++ b/src/Refitter.Tests/CSharpClientGeneratorFactoryIntegrationTests.cs @@ -0,0 +1,209 @@ +using FluentAssertions; +using NJsonSchema; +using NJsonSchema.CodeGeneration.CSharp; +using NSwag; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests; + +public class CSharpClientGeneratorFactoryIntegrationTests +{ + [Test] + public async Task Create_AppliesMutatorsInOrder_BeforeBuildingGenerator() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "Vehicle": { + "oneOf": [ + { "$ref": "#/components/schemas/Car" } + ], + "discriminator": { "propertyName": "type" } + }, + "Car": { + "type": "object", + "properties": { + "id": { "format": "int32" }, + "count": { "type": "integer" } + } + } + } + } + } + """); + + var settings = new RefitGeneratorSettings + { + Namespace = "TestNamespace", + CodeGeneratorSettings = new CodeGeneratorSettings + { + IntegerType = IntegerType.Int64, + }, + }; + + var mutators = new IOpenApiDocumentMutator[] + { + new DisableAdditionalPropertiesMutator(settings.GenerateDefaultAdditionalProperties), + new OneOfDiscriminatorToAllOfMutator(), + new FixMissingIntegerTypesMutator(), + new CustomIntegerTypeMutator( + settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32), + }; + + var factory = new CSharpClientGeneratorFactory(settings, document, mutators); + var generator = factory.Create(); + + generator.Should().NotBeNull(); + generator.Settings.Should().NotBeNull(); + generator.Settings.CSharpGeneratorSettings.Should().NotBeNull(); + generator.Settings.CSharpGeneratorSettings.Namespace.Should().Be("TestNamespace"); + } + + [Test] + public async Task Create_WithExplicitMutators_AppliesAllMutations() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "missingType": { "format": "int32" }, + "integerNoFormat": { "type": "integer" } + } + } + } + } + } + """); + + var settings = new RefitGeneratorSettings + { + Namespace = "Test", + CodeGeneratorSettings = new CodeGeneratorSettings + { + IntegerType = IntegerType.Int64, + }, + }; + + var mutators = new IOpenApiDocumentMutator[] + { + new FixMissingIntegerTypesMutator(), + new CustomIntegerTypeMutator(IntegerType.Int64), + }; + + var factory = new CSharpClientGeneratorFactory(settings, document, mutators); + factory.Create(); + + var props = document.Components!.Schemas["TestModel"].ActualSchema.Properties; + + props["missingType"].ActualSchema.Type.Should().Be(JsonObjectType.Integer); + props["integerNoFormat"].ActualSchema.Format.Should().Be("int64"); + } + + [Test] + public async Task Create_WithDefaultMutators_UsesStandardMutationOrder() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "Vehicle": { + "oneOf": [ + { "$ref": "#/components/schemas/Car" } + ], + "discriminator": { "propertyName": "type" } + }, + "Car": { + "type": "object", + "properties": { "name": { "type": "string" } } + } + } + } + } + """); + + var settings = new RefitGeneratorSettings + { + Namespace = "Test", + UsePolymorphicSerialization = true, + }; + + var factory = new CSharpClientGeneratorFactory(settings, document); + var generator = factory.Create(); + + var vehicle = document.Components!.Schemas["Vehicle"].ActualSchema; + vehicle.OneOf.Should().BeEmpty(); + + generator.Settings.CSharpGeneratorSettings.TemplateFactory + .Should().NotBeNull(); + } + + [Test] + public async Task Create_WithCodeGeneratorSettings_AppliesExplictPropertyCopies() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {} + } + """); + + var settings = new RefitGeneratorSettings + { + Namespace = "Test", + CodeGeneratorSettings = new CodeGeneratorSettings + { + GenerateDataAnnotations = false, + ExcludedTypeNames = new[] { "SomeType" }, + GenerateNativeRecords = true, + }, + }; + + var factory = new CSharpClientGeneratorFactory(settings, document); + var generator = factory.Create(); + + generator.Settings.CSharpGeneratorSettings.GenerateDataAnnotations + .Should().BeFalse(); + generator.Settings.CSharpGeneratorSettings.ExcludedTypeNames + .Should().Contain("SomeType"); + generator.Settings.CSharpGeneratorSettings.GenerateNativeRecords + .Should().BeTrue(); + } + + [Test] + public async Task Create_WithoutCodeGeneratorSettings_DoesNotThrow() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {} + } + """); + + var settings = new RefitGeneratorSettings + { + Namespace = "TestNamespace", + }; + + var factory = new CSharpClientGeneratorFactory(settings, document); + var generator = factory.Create(); + + generator.Should().NotBeNull(); + generator.Settings.CSharpGeneratorSettings.Namespace.Should().Be("TestNamespace"); + } +} diff --git a/src/Refitter.Tests/CustomIntegerTypeMutatorTests.cs b/src/Refitter.Tests/CustomIntegerTypeMutatorTests.cs new file mode 100644 index 00000000..75ee2b63 --- /dev/null +++ b/src/Refitter.Tests/CustomIntegerTypeMutatorTests.cs @@ -0,0 +1,169 @@ +using FluentAssertions; +using NJsonSchema; +using NSwag; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests; + +public class CustomIntegerTypeMutatorTests +{ + [Test] + public async Task Mutate_WithInt64_AddsInt64FormatToIntegerSchemasWithoutFormat() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "count": { "type": "integer" } + } + } + } + } + } + """); + + var sut = new CustomIntegerTypeMutator(IntegerType.Int64); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["count"] + .ActualSchema; + + schema.Format.Should().Be("int64"); + } + + [Test] + public async Task Mutate_WithInt32_DoesNotAddFormat() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "count": { "type": "integer" } + } + } + } + } + } + """); + + var sut = new CustomIntegerTypeMutator(IntegerType.Int32); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["count"] + .ActualSchema; + + schema.Format.Should().BeNullOrEmpty(); + } + + [Test] + public async Task Mutate_WithInt64_DoesNotChangeIntegerSchemaWithExistingFormat() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "count": { "type": "integer", "format": "int32" } + } + } + } + } + } + """); + + var sut = new CustomIntegerTypeMutator(IntegerType.Int64); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["count"] + .ActualSchema; + + schema.Format.Should().Be("int32"); + } + + [Test] + public async Task Mutate_WithInt64_OnlyAffectsIntegerSchemas() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "count": { "type": "integer" }, + "name": { "type": "string" }, + "price": { "type": "number" } + } + } + } + } + } + """); + + var sut = new CustomIntegerTypeMutator(IntegerType.Int64); + sut.Mutate(document); + + var props = document.Components!.Schemas["TestModel"].ActualSchema.Properties; + props["count"].ActualSchema.Format.Should().Be("int64"); + props["name"].ActualSchema.Format.Should().BeNullOrEmpty(); + props["price"].ActualSchema.Format.Should().BeNullOrEmpty(); + } + + [Test] + public async Task Mutate_WithInt64_AppliesToArrayItemSchemas() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { "type": "integer" } + } + } + } + } + } + } + """); + + var sut = new CustomIntegerTypeMutator(IntegerType.Int64); + sut.Mutate(document); + + var items = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["items"] + .ActualSchema.Item!.ActualSchema; + + items.Format.Should().Be("int64"); + } +} diff --git a/src/Refitter.Tests/DisableAdditionalPropertiesMutatorTests.cs b/src/Refitter.Tests/DisableAdditionalPropertiesMutatorTests.cs new file mode 100644 index 00000000..05ba4988 --- /dev/null +++ b/src/Refitter.Tests/DisableAdditionalPropertiesMutatorTests.cs @@ -0,0 +1,116 @@ +using FluentAssertions; +using NSwag; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests; + +public class DisableAdditionalPropertiesMutatorTests +{ + [Test] + public async Task Mutate_WithGenerateDefaultAdditionalPropertiesFalse_SetsAllowAdditionalPropertiesToFalse() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "id": { "type": "integer" } + } + } + } + } + } + """); + + var sut = new DisableAdditionalPropertiesMutator(generateDefaultAdditionalProperties: false); + sut.Mutate(document); + + document.Components!.Schemas["TestModel"].ActualSchema.AllowAdditionalProperties + .Should().BeFalse(); + } + + [Test] + public async Task Mutate_WithGenerateDefaultAdditionalPropertiesTrue_DoesNotChangeAllowAdditionalProperties() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "id": { "type": "integer" } + } + } + } + } + } + """); + + var expected = document.Components!.Schemas["TestModel"].ActualSchema.AllowAdditionalProperties; + + var sut = new DisableAdditionalPropertiesMutator(generateDefaultAdditionalProperties: true); + sut.Mutate(document); + + document.Components!.Schemas["TestModel"].ActualSchema.AllowAdditionalProperties + .Should().Be(expected); + } + + [Test] + public async Task Mutate_WithNoComponentsSchemas_DoesNotThrow() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {} + } + """); + + var sut = new DisableAdditionalPropertiesMutator(generateDefaultAdditionalProperties: false); + var act = () => sut.Mutate(document); + + act.Should().NotThrow(); + } + + [Test] + public async Task Mutate_WithGenerateDefaultAdditionalPropertiesFalse_AppliesToAllSchemas() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "ModelA": { + "type": "object", + "properties": { "a": { "type": "string" } } + }, + "ModelB": { + "type": "object", + "properties": { "b": { "type": "integer" } } + } + } + } + } + """); + + var sut = new DisableAdditionalPropertiesMutator(generateDefaultAdditionalProperties: false); + sut.Mutate(document); + + document.Components!.Schemas["ModelA"].ActualSchema.AllowAdditionalProperties + .Should().BeFalse(); + document.Components!.Schemas["ModelB"].ActualSchema.AllowAdditionalProperties + .Should().BeFalse(); + } +} diff --git a/src/Refitter.Tests/FixMissingIntegerTypesMutatorTests.cs b/src/Refitter.Tests/FixMissingIntegerTypesMutatorTests.cs new file mode 100644 index 00000000..15be5890 --- /dev/null +++ b/src/Refitter.Tests/FixMissingIntegerTypesMutatorTests.cs @@ -0,0 +1,198 @@ +using FluentAssertions; +using NJsonSchema; +using NSwag; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests; + +public class FixMissingIntegerTypesMutatorTests +{ + [Test] + public async Task Mutate_WithFormatInt32AndNoType_SetsTypeToInteger() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "formattedId": { "format": "int32" } + } + } + } + } + } + """); + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["formattedId"] + .ActualSchema; + + schema.Type.Should().Be(JsonObjectType.Integer); + } + + [Test] + public async Task Mutate_WithFormatInt64AndNoType_SetsTypeToInteger() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "bigId": { "format": "int64" } + } + } + } + } + } + """); + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["bigId"] + .ActualSchema; + + schema.Type.Should().Be(JsonObjectType.Integer); + } + + [Test] + public async Task Mutate_WithFormatFloatAndNoType_SetsTypeToNumber() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "price": { "format": "float" } + } + } + } + } + } + """); + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["price"] + .ActualSchema; + + schema.Type.Should().Be(JsonObjectType.Number); + } + + [Test] + public async Task Mutate_WithFormatDoubleAndNoType_SetsTypeToNumber() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "score": { "format": "double" } + } + } + } + } + } + """); + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["score"] + .ActualSchema; + + schema.Type.Should().Be(JsonObjectType.Number); + } + + [Test] + public async Task Mutate_WithFormatInt32AndExistingType_DoesNotChangeType() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "count": { "type": "integer", "format": "int32" } + } + } + } + } + } + """); + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + var schema = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["count"] + .ActualSchema; + + schema.Type.Should().Be(JsonObjectType.Integer); + } + + [Test] + public async Task Mutate_WithoutFormat_DoesNotChangeType() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { + "name": { "type": "string" } + } + } + } + } + } + """); + + var expectedType = document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["name"] + .ActualSchema.Type; + + var sut = new FixMissingIntegerTypesMutator(); + sut.Mutate(document); + + document.Components!.Schemas["TestModel"] + .ActualSchema.Properties["name"] + .ActualSchema.Type.Should().Be(expectedType); + } +} diff --git a/src/Refitter.Tests/OneOfDiscriminatorToAllOfMutatorTests.cs b/src/Refitter.Tests/OneOfDiscriminatorToAllOfMutatorTests.cs new file mode 100644 index 00000000..318aefc9 --- /dev/null +++ b/src/Refitter.Tests/OneOfDiscriminatorToAllOfMutatorTests.cs @@ -0,0 +1,169 @@ +using FluentAssertions; +using NSwag; +using Refitter.Core; +using TUnit.Core; + +namespace Refitter.Tests; + +public class OneOfDiscriminatorToAllOfMutatorTests +{ + [Test] + public async Task Mutate_WithOneOfAndDiscriminator_AddsAllOfToSubtypes() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "Vehicle": { + "oneOf": [ + { "$ref": "#/components/schemas/Car" }, + { "$ref": "#/components/schemas/Truck" } + ], + "discriminator": { "propertyName": "type" } + }, + "Car": { + "type": "object", + "properties": { "wheels": { "type": "integer" } } + }, + "Truck": { + "type": "object", + "properties": { "capacity": { "type": "number" } } + } + } + } + } + """); + + var sut = new OneOfDiscriminatorToAllOfMutator(); + sut.Mutate(document); + + var vehicle = document.Components!.Schemas["Vehicle"].ActualSchema; + var car = document.Components!.Schemas["Car"].ActualSchema; + var truck = document.Components!.Schemas["Truck"].ActualSchema; + + vehicle.OneOf.Should().BeEmpty(); + vehicle.Type.Should().Be(NJsonSchema.JsonObjectType.Object); + + car.AllOf.Should().Contain(a => a.HasReference && a.ActualSchema == vehicle); + truck.AllOf.Should().Contain(a => a.HasReference && a.ActualSchema == vehicle); + } + + [Test] + public async Task Mutate_WithAnyOfAndDiscriminator_AddsAllOfToSubtypes() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "Payment": { + "anyOf": [ + { "$ref": "#/components/schemas/CreditCard" }, + { "$ref": "#/components/schemas/BankTransfer" } + ], + "discriminator": { "propertyName": "type" } + }, + "CreditCard": { + "type": "object", + "properties": { "number": { "type": "string" } } + }, + "BankTransfer": { + "type": "object", + "properties": { "account": { "type": "string" } } + } + } + } + } + """); + + var sut = new OneOfDiscriminatorToAllOfMutator(); + sut.Mutate(document); + + var payment = document.Components!.Schemas["Payment"].ActualSchema; + var creditCard = document.Components!.Schemas["CreditCard"].ActualSchema; + var bankTransfer = document.Components!.Schemas["BankTransfer"].ActualSchema; + + payment.AnyOf.Should().BeEmpty(); + payment.Type.Should().Be(NJsonSchema.JsonObjectType.Object); + + creditCard.AllOf.Should().Contain(a => a.HasReference && a.ActualSchema == payment); + bankTransfer.AllOf.Should().Contain(a => a.HasReference && a.ActualSchema == payment); + } + + [Test] + public async Task Mutate_WithoutDiscriminator_DoesNotChangeSchemas() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestModel": { + "type": "object", + "properties": { "name": { "type": "string" } } + } + } + } + } + """); + + var expectedOneOf = document.Components!.Schemas["TestModel"].ActualSchema.OneOf.Count; + + var sut = new OneOfDiscriminatorToAllOfMutator(); + sut.Mutate(document); + + document.Components!.Schemas["TestModel"].ActualSchema.OneOf.Count + .Should().Be(expectedOneOf); + } + + [Test] + public async Task Mutate_WithNoComponentsSchemas_DoesNotThrow() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {} + } + """); + + var sut = new OneOfDiscriminatorToAllOfMutator(); + var act = () => sut.Mutate(document); + + act.Should().NotThrow(); + } + + [Test] + public async Task Mutate_WithDiscriminatorButNoUnionSchemas_DoesNotChange() + { + var document = await OpenApiDocument.FromJsonAsync(""" + { + "openapi": "3.0.1", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "Base": { + "type": "object", + "discriminator": { "propertyName": "type" }, + "properties": { "name": { "type": "string" } } + } + } + } + } + """); + + var sut = new OneOfDiscriminatorToAllOfMutator(); + sut.Mutate(document); + + document.Components!.Schemas["Base"].ActualSchema.Type + .Should().Be(NJsonSchema.JsonObjectType.Object); + } +}