Skip to content

Commit

Permalink
feat: better type-value swagger objects (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alxandr authored Aug 22, 2024
1 parent f64c27b commit 7071c9e
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public OpenApiExampleProvider(
public IEnumerable<IOpenApiAny>? GetExample<T>()
=> GetExample(typeof(T));

/// <summary>
/// Gets an example for the specified type mapped by an optional mapper.
/// </summary>
/// <typeparam name="T">The type to get an example for.</typeparam>
/// <typeparam name="U">The mapped type.</typeparam>
/// <param name="mapper">A mapper to apply to all example items.</param>
/// <returns>An enumerable of <see cref="IOpenApiAny"/> example data.</returns>
public IEnumerable<IOpenApiAny>? GetExample<T, U>(Func<T, U> mapper)
=> GetExample(typeof(T), typeof(U), x => x is null ? null : mapper((T)x));

/// <summary>
/// Gets an example for the specified type.
/// </summary>
Expand All @@ -45,18 +55,38 @@ public OpenApiExampleProvider(
var exampleDataOptions = _exampleDataOptions.CurrentValue ?? ExampleDataOptions.DefaultOptions;
var jsonOptions = _jsonOptions.CurrentValue?.SerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);

return GetExample(type, exampleDataOptions, jsonOptions);
return GetExample(type, type, mapper: null, exampleDataOptions, jsonOptions);
}

/// <summary>
/// Gets an example for the specified type mapped by an optional mapper.
/// </summary>
/// <param name="type">The type to get an example for.</param>
/// <param name="mappedType">The type produced by the mapper.</param>
/// <param name="mapper">A mapper to apply to all example items.</param>
/// <returns>An enumerable of <see cref="IOpenApiAny"/> example data.</returns>
public IEnumerable<IOpenApiAny>? GetExample(Type type, Type mappedType, Func<object?, object?> mapper)
{
var exampleDataOptions = _exampleDataOptions.CurrentValue ?? ExampleDataOptions.DefaultOptions;
var jsonOptions = _jsonOptions.CurrentValue?.SerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);

return GetExample(type, mappedType, mapper, exampleDataOptions, jsonOptions);
}

private static IEnumerable<IOpenApiAny>? GetExample(Type type, ExampleDataOptions exampleDataOptions, JsonSerializerOptions jsonSerializerOptions)
private static IEnumerable<IOpenApiAny>? GetExample(Type type, Type mappedType, Func<object?, object?>? mapper, ExampleDataOptions exampleDataOptions, JsonSerializerOptions jsonSerializerOptions)
{
var examples = ExampleData.GetExamples(type, exampleDataOptions);
if (examples is null)
{
return null;
}

return ConvertExamples(examples, type, jsonSerializerOptions);
if (mapper is not null)
{
examples = examples.Cast<object>().Select(mapper);
}

return ConvertExamples(examples, mappedType, jsonSerializerOptions);
}

private static IEnumerable<IOpenApiAny> ConvertExamples(IEnumerable examples, Type type, JsonSerializerOptions jsonSerializerOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private static bool IsGenericConstructedTypeOf(Type iface, Type genericDef)

private static bool IsStringCompatible(Type typeToConvert, Type[] interfaces)
{
if (!typeToConvert.IsAssignableTo(typeof(ISpanFormattable)))
if (!typeToConvert.IsAssignableTo(typeof(IFormattable)))
{
return false;
}
Expand Down Expand Up @@ -239,7 +239,7 @@ private class StringConverter<T>

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
throw new NotImplementedException();
WriteAsString(writer, value);
}

protected static T? ReadAsString(ref Utf8JsonReader reader)
Expand Down
148 changes: 104 additions & 44 deletions src/Altinn.Urn/src/Altinn.Urn.Swashbuckle/UrnSwaggerFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context)
GetFilterFor(urnType)?.ApplyUrnSchemaFilter(schema, urnType, context, _openApiExampleProvider);
return;
}
else if (definition == typeof(UrnJsonTypeValue<>))
else if (definition == typeof(UrnJsonTypeValue<>) || definition == typeof(UrnJsonTypeValueVariant<>))
{
var urnType = type.GetGenericArguments()[0];
GetFilterFor(urnType)?.ApplyUrnTypeValueObjectSchemaFilter(schema, urnType, context, _openApiExampleProvider);
Expand Down Expand Up @@ -134,55 +134,21 @@ public override void ApplyUrnSchemaFilter(OpenApiSchema schema, Type type, Schem

public override void ApplyUrnTypeValueObjectSchemaFilter(OpenApiSchema schema, Type type, SchemaFilterContext context, OpenApiExampleProvider exampleProvider)
{
TVariants variant = default;

if (type != typeof(TUrn))
{
// we're dealing with a variant type
Debug.Assert(typeof(TUrn).IsAssignableFrom(type));
foreach (var v in TUrn.Variants)
{
if (TUrn.VariantTypeFor(v) == type)
{
variant = v;
break;
}
}
}

ReadOnlySpan<TVariants> variants = [variant];

if (type == typeof(TUrn))
{
variants = TUrn.Variants;
ApplyBaseUrnTypeValueObjectFilter(schema, type, context, exampleProvider);
return;
}

var keySchema = new OpenApiSchema
{
Type = "string",
Enum = [],
};

foreach (var v in variants)
Debug.Assert(typeof(TUrn).IsAssignableFrom(type));
foreach (var variant in TUrn.Variants)
{
keySchema.Enum.Add(new OpenApiString(TUrn.CanonicalPrefixFor(v)));
if (TUrn.VariantTypeFor(variant) == type)
{
ApplyVariantUrnTypeValueObjectFilter(schema, type, context, variant, exampleProvider);
return;
}
}

var valueSchema = new OpenApiSchema
{
Type = "string",
};

// reset defaults
schema.Properties.Clear();
schema.Required.Clear();
schema.AdditionalPropertiesAllowed = false;
schema.AdditionalProperties = null;
schema.Type = "object";
schema.Properties.Add("type", keySchema);
schema.Properties.Add("value", valueSchema);
schema.Required.Add("type");
schema.Required.Add("value");
}

public override void ApplyUrnDictionarySchemaFilter(OpenApiSchema schema, Type type, SchemaFilterContext context, OpenApiExampleProvider exampleProvider)
Expand Down Expand Up @@ -234,6 +200,98 @@ public override void ApplyUrnDictionarySchemaFilter(OpenApiSchema schema, Type t
}
}

private static void ApplyBaseUrnTypeValueObjectFilter(OpenApiSchema schema, Type type, SchemaFilterContext context, OpenApiExampleProvider exampleProvider)
{
// reset defaults
schema.Properties.Clear();
schema.Required.Clear();
schema.AdditionalPropertiesAllowed = false;
schema.AdditionalProperties = null;
schema.Type = "object";

var typeValues = new List<IOpenApiAny>();
foreach (var prefix in TUrn.Prefixes)
{
typeValues.Add(new OpenApiString(prefix));
}

var mapping = new Dictionary<string, string>();
var oneOf = schema.OneOf;
oneOf.Clear();
schema.Discriminator = new OpenApiDiscriminator
{
PropertyName = "type",
Mapping = mapping,
};
schema.Properties.Add("type", new OpenApiSchema
{
Type = "string",
Enum = typeValues,
});
schema.Required.Add("type");

foreach (var variant in TUrn.Variants)
{
var variantType = typeof(UrnJsonTypeValueVariant<>).MakeGenericType(TUrn.VariantTypeFor(variant));
if (!context.SchemaRepository.TryLookupByType(variantType, out var referenceSchema))
{
referenceSchema = context.SchemaGenerator.GenerateSchema(variantType, context.SchemaRepository);
}

oneOf.Add(referenceSchema);
foreach (var prefix in TUrn.PrefixesFor(variant))
{
mapping.Add(prefix, referenceSchema.Reference.ReferenceV3);
}
}

schema.Example = exampleProvider.GetExample(typeof(UrnJsonTypeValue<>).MakeGenericType(type))?.FirstOrDefault();
}

private static void ApplyVariantUrnTypeValueObjectFilter(OpenApiSchema schema, Type type, SchemaFilterContext context, TVariants variant, OpenApiExampleProvider exampleProvider)
{
// reset defaults
schema.Properties.Clear();
schema.Required.Clear();
schema.AdditionalPropertiesAllowed = false;
schema.AdditionalProperties = null;
schema.Type = "object";

var typeValues = new List<IOpenApiAny>();
foreach (var prefix in TUrn.PrefixesFor(variant))
{
typeValues.Add(new OpenApiString(prefix));
}

schema.OneOf.Clear();
schema.Properties.Add("type", new OpenApiSchema
{
Type = "string",
Enum = typeValues,
});
schema.Required.Add("type");

var valueType = TUrn.ValueTypeFor(variant);
var valueSchema = new OpenApiSchema
{
Type = "string",
};

schema.Properties.Add("value", valueSchema);
schema.Required.Add("value");

schema.Example = new OpenApiObject
{
["type"] = new OpenApiString(TUrn.CanonicalPrefixFor(variant)),
["value"] = exampleProvider.GetExample(valueType, typeof(string), static (object? v) => v switch
{
null => null,
IFormattable f => f.ToString(format: null, formatProvider: null),
_ => v?.ToString() ?? "string",
})?.FirstOrDefault() ?? new OpenApiString("string"),
};
}

private static void ApplyBaseUrnFilter(OpenApiSchema schema, Type type, SchemaFilterContext context, OpenApiExampleProvider exampleProvider)
{
// reset defaults
Expand Down Expand Up @@ -317,4 +375,6 @@ private static string GetPattern(ReadOnlySpan<string> prefixes)
return builder.ToString();
}
}

private abstract class UrnJsonTypeValueVariant<T>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public abstract partial record PersonUrn
[UrnKey("altinn:organization:org-no")]
public partial bool IsOrganizationNo(out OrgNo orgNo);

[UrnKey("altinn:party:id")]
[UrnKey("altinn:party:id", Canonical = true)]
[UrnKey("altinn:party-old:id")]
public partial bool IsPartyId(out int partyId);

[UrnKey("altinn:party:uuid")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.Swashbuckle.Examples;
using Altinn.Urn.Json;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
Expand Down Expand Up @@ -63,6 +64,14 @@ public void Apply_ForUrn_CreatesVariantsWithExamples()
}
}

[Fact]
public void Apply_ForJsonTypeValueObjectUrn_CreatesDiscriminator()
{
var schema = SchemaFor<UrnJsonTypeValue<PersonUrn>>();

schema.Discriminator.Should().NotBeNull();
}

//private ISchemaFilter SchemaFilter => _sut;
private OpenApiSchema SchemaFor(Type type)
{
Expand Down

0 comments on commit 7071c9e

Please sign in to comment.