diff --git a/Directory.Build.props b/Directory.Build.props index 9e2f1a32..ef55a663 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,15 +30,15 @@ 0.2.0 0.2.0 0.3.0 - 0.17.1 + 0.18.0 0.2.0 - 0.24.0 + 0.25.0 0.12.1 0.9.0 0.9.0 0.9.0 - 0.9.0 + 0.10.0 diff --git a/generators/csharp/OpenApiCodegen.CSharp.Base/Client/Templates/client.handlebars b/generators/csharp/OpenApiCodegen.CSharp.Base/Client/Templates/client.handlebars index 676f943d..72ca6ccc 100644 --- a/generators/csharp/OpenApiCodegen.CSharp.Base/Client/Templates/client.handlebars +++ b/generators/csharp/OpenApiCodegen.CSharp.Base/Client/Templates/client.handlebars @@ -19,14 +19,14 @@ namespace {{PackageName}} /// {{Description}}{{/each}} public static async global::System.Threading.Tasks.Task<{{Operation.Name}}ReturnType> {{Operation.Name}}( this global::System.Net.Http.HttpMessageInvoker client, - {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}, + {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}, {{/each}} global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken) ) => await (await client.SendAsync({{Operation.Name}}({{#each RequestBody.AllParams}}{{ParamName}}{{#unless @last}},{{/unless}}{{/each}}), cancellationToken).ConfigureAwait(false)) .Parse{{Operation.Name}}().ConfigureAwait(false); public static global::System.Net.Http.HttpRequestMessage {{Operation.Name}}( - {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}{{#unless @last}},{{/unless}} + {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}{{#unless @last}},{{/unless}} {{/each}}) { {{#if RequestBody.HasQueryParam}} diff --git a/generators/csharp/OpenApiCodegen.CSharp.Base/MvcServer/Templates/controller.handlebars b/generators/csharp/OpenApiCodegen.CSharp.Base/MvcServer/Templates/controller.handlebars index 5e280661..a81ef29f 100644 --- a/generators/csharp/OpenApiCodegen.CSharp.Base/MvcServer/Templates/controller.handlebars +++ b/generators/csharp/OpenApiCodegen.CSharp.Base/MvcServer/Templates/controller.handlebars @@ -57,7 +57,7 @@ namespace {{PackageName}} }}{{#if HasMaxLength}}, global::System.ComponentModel.DataAnnotations.MaxLength({{MaxLength}}){{/if}}{{! }}{{#if HasMinimum}}{{#if HasMaximum}}, global::System.ComponentModel.DataAnnotations.Range({{Minimum}}, {{Maximum}}){{/if}}{{/if}}{{! }}] {{#unless Optional}}{{{DataType}}}{{else - }}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{{DataType}}}>{{/unless}} {{ParamName}}{{#unless @last}}, + }}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{{DataType}}}>{{/unless}} {{ParamName}}{{#unless @last}}, {{/unless}}{{/unless}}{{/each}} ) => (await {{RequestBody.Name}}({{#each RequestBody.AllParams}}{{ParamName}}{{#unless @last}},{{/unless}}{{/each}})).ActionResult; {{!-- @@ -69,7 +69,7 @@ namespace {{PackageName}} protected abstract global::System.Threading.Tasks.Task<{{Operation.Name}}ActionResult> {{RequestBody.Name}}({{#each RequestBody.AllParams}} {{#unless Optional}}{{{DataType}}}{{else - }}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{{DataType}}}>{{/unless}} {{ParamName}}{{#unless @last}}, {{/unless}}{{/each}}); + }}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{{DataType}}}>{{/unless}} {{ParamName}}{{#unless @last}}, {{/unless}}{{/each}}); {{/each}} public readonly struct {{Name}}ActionResult diff --git a/generators/csharp/OpenApiCodegen.CSharp.Base/WebhookClient/Templates/webhooks.handlebars b/generators/csharp/OpenApiCodegen.CSharp.Base/WebhookClient/Templates/webhooks.handlebars index 89efa800..f1dacf6a 100644 --- a/generators/csharp/OpenApiCodegen.CSharp.Base/WebhookClient/Templates/webhooks.handlebars +++ b/generators/csharp/OpenApiCodegen.CSharp.Base/WebhookClient/Templates/webhooks.handlebars @@ -20,7 +20,7 @@ namespace {{PackageName}} public static async global::System.Threading.Tasks.Task<{{Operation.Name}}ReturnType> {{Operation.Name}}( this global::System.Net.Http.HttpMessageInvoker client, global::System.Uri uri, - {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}, + {{#each RequestBody.AllParams}}{{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}}, {{/each}} global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken) ) => await (await client.SendAsync({{Operation.Name}}(uri{{#each RequestBody.AllParams}}, {{ParamName}}{{/each}}), cancellationToken).ConfigureAwait(false)) @@ -28,7 +28,7 @@ namespace {{PackageName}} public static global::System.Net.Http.HttpRequestMessage {{Operation.Name}}( global::System.Uri uri - {{#each RequestBody.AllParams}}, {{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}} + {{#each RequestBody.AllParams}}, {{#unless Required}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{/unless}}{{{DataType}}}{{#unless Required}}>?{{/unless}} {{ParamName}} {{/each}}) { {{#if RequestBody.HasQueryParam}} diff --git a/lib/OpenApi.CSharp/Templates/objectmodel.handlebars b/lib/OpenApi.CSharp/Templates/objectmodel.handlebars index 967efe19..615f70fb 100644 --- a/lib/OpenApi.CSharp/Templates/objectmodel.handlebars +++ b/lib/OpenApi.CSharp/Templates/objectmodel.handlebars @@ -19,7 +19,7 @@ namespace {{this.PackageName}} }}{{#if HasMinLength}}[global::System.ComponentModel.DataAnnotations.MinLength({{MinLength}})]{{/if}}{{! }}{{#if HasMaxLength}}[global::System.ComponentModel.DataAnnotations.MaxLength({{MaxLength}})]{{/if}}{{! }}{{#if HasMinimum}}{{#if HasMaximum}}[global::System.ComponentModel.DataAnnotations.Range({{Minimum}}, {{Maximum}})]{{/if}}{{/if}}{{! - data type: }} {{#if Optional}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.Optional<{{/if}}{{{DataType}}}{{#if Optional}}>?{{/if}}{{! + data type: }} {{#if Optional}}global::DarkPatterns.OpenApiCodegen.Json.Extensions.IOptional<{{/if}}{{{DataType}}}{{#if Optional}}>?{{/if}}{{! name: }} {{Name}}{{#unless @last}}, {{/unless}}{{/each}} ); {{/with}} diff --git a/lib/OpenApiCodegen.Json.Extensions.Test/OptionalShould.cs b/lib/OpenApiCodegen.Json.Extensions.Test/OptionalShould.cs index 372036d6..145ceb05 100644 --- a/lib/OpenApiCodegen.Json.Extensions.Test/OptionalShould.cs +++ b/lib/OpenApiCodegen.Json.Extensions.Test/OptionalShould.cs @@ -139,6 +139,36 @@ public void AllowUnwrappingOptional() Assert.Equal("foo", actual); } + [Fact] + public void AllowPatternMatchingOfOptional() + { + Optional sample = Optional.Create("foo"); + if (sample is { Value: var actual }) + Assert.Equal("foo", actual); + else + Assert.Fail("Pattern match failed"); + } + + [Fact] + public void AllowPatternMatchingOfIOptional() + { + IOptional sample = Optional.Create("foo"); + if (sample is { Value: var actual }) + Assert.Equal("foo", actual); + else + Assert.Fail("Pattern match failed"); + } + + [Fact] + public void AllowCovarianceOfIOptional() + { + IOptional sample = Optional.Create("foo"); + if (sample is { Value: var actual }) + Assert.Equal("foo", actual); + else + Assert.Fail("Pattern match failed"); + } + [Fact] public void AllowCreationOfOptionalWithEnumerables() { diff --git a/lib/OpenApiCodegen.Json.Extensions/Optional.cs b/lib/OpenApiCodegen.Json.Extensions/Optional.cs index eb9a10cf..e867cb05 100644 --- a/lib/OpenApiCodegen.Json.Extensions/Optional.cs +++ b/lib/OpenApiCodegen.Json.Extensions/Optional.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Reflection; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,18 +8,28 @@ namespace DarkPatterns.OpenApiCodegen.Json.Extensions; public static class Optional { public static Optional Create(T value) => new Optional.Present(value); - public static T GetValueOrThrow(Optional input) => + public static T GetValueOrThrow(IOptional input) => input.TryGet(out var result) ? result : throw new InvalidOperationException(); } [JsonConverter(typeof(OptionalJsonConverterFactory))] -public abstract record Optional +public interface IOptional +{ + T Value { get; } +} + +[JsonConverter(typeof(OptionalJsonConverterFactory))] +public abstract record Optional : IOptional { private Optional() { } public static readonly Optional? None = null; - public sealed record Present(T Value) : Optional; + public abstract T Value { get; init; } + + public sealed record Present(T Value) : Optional, IOptional + { + } public class Serializer : JsonConverter> { @@ -52,7 +60,8 @@ public class OptionalJsonConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { - return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + return typeToConvert.IsGenericType + && (typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>) || typeToConvert.GetGenericTypeDefinition() == typeof(IOptional<>)); } public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) diff --git a/lib/OpenApiCodegen.Json.Extensions/OptionalExtensions.cs b/lib/OpenApiCodegen.Json.Extensions/OptionalExtensions.cs index f4e66e13..a3a19931 100644 --- a/lib/OpenApiCodegen.Json.Extensions/OptionalExtensions.cs +++ b/lib/OpenApiCodegen.Json.Extensions/OptionalExtensions.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DarkPatterns.OpenApiCodegen.Json.Extensions; +namespace DarkPatterns.OpenApiCodegen.Json.Extensions; public static class OptionalExtensions { - public static bool TryGet(this Optional? input, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T value) + public static bool TryGet(this IOptional? input, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T value) { if (input is Optional.Present { Value: var presentValue }) { @@ -17,8 +13,8 @@ public static bool TryGet(this Optional? input, [System.Diagnostics.CodeAn return false; } - public static T? GetValueOrDefault(this Optional? input) => + public static T? GetValueOrDefault(this IOptional? input) => input.TryGet(out var result) ? result : default; - public static T GetValueOrDefault(this Optional? input, T defaultValue) => + public static T GetValueOrDefault(this IOptional? input, T defaultValue) => input.TryGet(out var result) ? result : defaultValue; } diff --git a/lib/TestApp.Test/OptionalYamlShould.cs b/lib/TestApp.Test/OptionalYamlShould.cs index e974e752..3833fe73 100644 --- a/lib/TestApp.Test/OptionalYamlShould.cs +++ b/lib/TestApp.Test/OptionalYamlShould.cs @@ -1,10 +1,6 @@ using Microsoft.AspNetCore.Mvc; using DarkPatterns.OpenApiCodegen.Json.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; -using System.Text; using System.Threading.Tasks; using Xunit; @@ -25,8 +21,8 @@ public Task Handle_mixed_nullable_optional_with_all_provided() => { Assert.True(controller.ModelState.IsValid); Assert.Equal(2, request.NullableOnly); - Assert.Equal?>(Optional.Create(3), request.OptionalOnly); - Assert.Equal?>(Optional.Create(5), request.OptionalOrNullable); + Assert.Equal?>(Optional.Create(3), request.OptionalOnly); + Assert.Equal?>(Optional.Create(5), request.OptionalOrNullable); }, AssertResponseMessage = VerifyResponse(200, new { nullableOnly = 2, optionalOnly = 3, optionalOrNullable = 5 }), });