Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 41 additions & 8 deletions codegen/internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ func (g *Generator) buildModels(doc *v3.Document) ([]modelTemplateData, error) {
if schema == nil {
continue
}
// For alias components (for example arrays/maps), resolve with an inline base so
// nested inline object items can still become generated models instead of JsonDocument.
// Resolve aliases with inline context so arrays/maps with inline object
// values generate concrete models instead of falling back to JsonDocument.
typeInfo, err := g.resolveInlineSchemaType(base.CreateSchemaProxy(schema), true, info.TypeName)
if err != nil {
return nil, err
Expand Down Expand Up @@ -704,24 +704,57 @@ func (g *Generator) convertParameter(param *v3.Parameter) (parameterTemplateData
}
required := param.Required != nil && *param.Required
typeInfo := g.resolveType(param.Schema, required)
defaultValue := ""
if !required {
defaultValue = " = null"
}

argName := naming.Identifier(param.Name)
declaration := ""
if shouldUseOptionalQueryParameter(param, required, typeInfo) {
baseType := strings.TrimSuffix(typeInfo.TypeName, "?")
declaration = fmt.Sprintf("OptionalQuery<%s> %s = default", baseType, argName)
} else {
defaultValue := ""
if !required {
defaultValue = " = null"
}
declaration = fmt.Sprintf("%s %s%s", typeInfo.TypeName, argName, defaultValue)
}
return parameterTemplateData{
Location: param.In,
Name: param.Name,
ArgName: argName,
Declaration: fmt.Sprintf("%s %s%s", typeInfo.TypeName, argName, defaultValue),
Declaration: declaration,
Description: sanitizeText(param.Description),
Required: required,
BuilderCall: builderCall(param.In, param.Name, argName),
IsCollection: typeInfo.IsCollection,
}, nil
}

func shouldUseOptionalQueryParameter(param *v3.Parameter, required bool, typeInfo typeInfo) bool {
if param == nil || required || param.In != "query" {
return false
}
if typeInfo.IsValueType || typeInfo.IsCollection {
return false
}
return schemaAllowsNull(param.Schema)
}

func schemaAllowsNull(schemaRef *base.SchemaProxy) bool {
if schemaRef == nil {
return false
}
schema := schemaRef.Schema()
if schema == nil {
return false
}
if schema.Nullable != nil && *schema.Nullable {
return true
}
if len(schema.AllOf) == 1 {
return schemaAllowsNull(schema.AllOf[0])
}
return false
}

func (g *Generator) buildRequestBody(clientName, methodName string, body *v3.RequestBody) (*bodyTemplateData, error) {
if body == nil {
return nil, nil
Expand Down
20 changes: 20 additions & 0 deletions src/SumUp.Tests/RequestBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,24 @@ public void Build_IncludesRepeatedQueryParameters()

Assert.Equal("https://api.sumup.com/v0.1/items?status=open&status=closed", request.RequestUri!.AbsoluteUri);
}

[Fact]
public void Build_OmitsUnsetOptionalQuery()
{
var builder = new RequestBuilder(HttpMethod.Get, "/v0.1/items", new Uri("https://api.sumup.com"));
builder.AddQuery("status", OptionalQuery<string>.Unset);
var request = builder.Build();

Assert.Equal("https://api.sumup.com/v0.1/items", request.RequestUri!.AbsoluteUri);
}

[Fact]
public void Build_EmitsExplicitNullOptionalQuery()
{
var builder = new RequestBuilder(HttpMethod.Get, "/v0.1/items", new Uri("https://api.sumup.com"));
builder.AddQuery("status", OptionalQuery<string>.Null());
var request = builder.Build();

Assert.Equal("https://api.sumup.com/v0.1/items?status=null", request.RequestUri!.AbsoluteUri);
}
}
16 changes: 16 additions & 0 deletions src/SumUp/Http/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ internal void AddQuery(string name, object? value)
_query.Add(new KeyValuePair<string, string>(name, ConvertToString(value)));
}

internal void AddQuery<T>(string name, OptionalQuery<T> value)
{
if (!value.IsSet)
{
return;
}

if (value.IsNull)
{
_query.Add(new KeyValuePair<string, string>(name, "null"));
return;
}

AddQuery(name, value.RawValue);
}

internal void AddHeader(string name, object? value)
{
if (value is null)
Expand Down
4 changes: 2 additions & 2 deletions src/SumUp/MembershipsClient.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal MembershipsClient(ApiClient client)
/// <param name="roles">Filter the returned memberships by role.</param>
/// <param name="requestOptions">Optional per-request overrides.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
public ApiResponse<MembershipsListResponse> List(int? offset = null, int? limit = null, string? kind = null, MembershipStatus? status = null, string? resourceType = null, bool? resourceAttributesSandbox = null, string? resourceName = null, string? resourceParentId = null, string? resourceParentType = null, IEnumerable<string>? roles = null, RequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public ApiResponse<MembershipsListResponse> List(int? offset = null, int? limit = null, string? kind = null, MembershipStatus? status = null, string? resourceType = null, bool? resourceAttributesSandbox = null, string? resourceName = null, OptionalQuery<string> resourceParentId = default, OptionalQuery<string> resourceParentType = default, IEnumerable<string>? roles = null, RequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
var request = _client.CreateRequest(HttpMethod.Get, "/v0.1/memberships", builder =>
{
Expand Down Expand Up @@ -95,7 +95,7 @@ public ApiResponse<MembershipsListResponse> List(int? offset = null, int? limit
/// <param name="roles">Filter the returned memberships by role.</param>
/// <param name="requestOptions">Optional per-request overrides.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
public async Task<ApiResponse<MembershipsListResponse>> ListAsync(int? offset = null, int? limit = null, string? kind = null, MembershipStatus? status = null, string? resourceType = null, bool? resourceAttributesSandbox = null, string? resourceName = null, string? resourceParentId = null, string? resourceParentType = null, IEnumerable<string>? roles = null, RequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public async Task<ApiResponse<MembershipsListResponse>> ListAsync(int? offset = null, int? limit = null, string? kind = null, MembershipStatus? status = null, string? resourceType = null, bool? resourceAttributesSandbox = null, string? resourceName = null, OptionalQuery<string> resourceParentId = default, OptionalQuery<string> resourceParentType = default, IEnumerable<string>? roles = null, RequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
var request = _client.CreateRequest(HttpMethod.Get, "/v0.1/memberships", builder =>
{
Expand Down
42 changes: 42 additions & 0 deletions src/SumUp/OptionalQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace SumUp;

/// <summary>
/// Represents a query parameter that can be omitted, set to a value, or set explicitly to null.
/// </summary>
public readonly struct OptionalQuery<T>
{
internal object? RawValue { get; }

/// <summary>
/// Indicates whether the query parameter should be emitted.
/// </summary>
public bool IsSet { get; }

/// <summary>
/// Indicates whether the query parameter should be emitted with a literal null value.
/// </summary>
public bool IsNull => IsSet && RawValue is null;

private OptionalQuery(bool isSet, object? value)
{
IsSet = isSet;
RawValue = value;
}

/// <summary>
/// Omits the parameter from the query string.
/// </summary>
public static OptionalQuery<T> Unset => default;

/// <summary>
/// Emits the parameter as an explicit null literal.
/// </summary>
public static OptionalQuery<T> Null() => new(isSet: true, value: null);

/// <summary>
/// Emits the parameter with the provided value.
/// </summary>
public static OptionalQuery<T> From(T value) => new(isSet: true, value: value);

public static implicit operator OptionalQuery<T>(T value) => From(value);
}
Loading