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

Generating IObservable type response #322

Merged
merged 1 commit into from
Feb 26, 2024
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
21 changes: 15 additions & 6 deletions src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ private string GenerateInterfaceBody()
protected string GetTypeName(OpenApiOperation operation)
{
if (settings.ResponseTypeOverride.TryGetValue(operation.OperationId, out var type))
return type is null or "void" ? "Task" : $"Task<{WellKnownNamesspaces.TrimImportedNamespaces(type)}>";
{
return type is null or "void" ? GetAsyncOperationType(true) : $"{GetAsyncOperationType(false)}<{WellKnownNamesspaces.TrimImportedNamespaces(type)}>";
}

var returnTypeParameter =
var returnTypeParameter =
(new[] { "200", "201", "203", "206" })
.Where(operation.Responses.ContainsKey)
.Select(code => GetTypeName(code, operation))
Expand Down Expand Up @@ -172,9 +174,10 @@ protected string GetReturnType(string? returnTypeParameter)

private string GetDefaultReturnType()
{
var asyncType = GetAsyncOperationType(true);
return settings.ReturnIApiResponse
? "Task<IApiResponse>"
: "Task";
? $"{asyncType}<IApiResponse>"
: asyncType;
}

/// <summary>
Expand All @@ -193,11 +196,17 @@ protected static bool IsApiResponseType(string typeName)

private string GetConfiguredReturnType(string returnTypeParameter)
{
var asyncType = GetAsyncOperationType(false);
return settings.ReturnIApiResponse
? $"Task<IApiResponse<{WellKnownNamesspaces.TrimImportedNamespaces(returnTypeParameter)}>>"
: $"Task<{WellKnownNamesspaces.TrimImportedNamespaces(returnTypeParameter)}>";
? $"{asyncType}<IApiResponse<{WellKnownNamesspaces.TrimImportedNamespaces(returnTypeParameter)}>>"
: $"{asyncType}<{WellKnownNamesspaces.TrimImportedNamespaces(returnTypeParameter)}>";
}

private string GetAsyncOperationType(bool withVoidReturnType) =>
settings.ReturnIObservable
? "IObservable" + (withVoidReturnType ? "<Unit>" : string.Empty)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much experience working with IObservable<T> but why do we need Unit? What does it do and what is it for?

The examples in the Refit README uses an HttpResponseMessage together with IObservable

// Returns the raw response, as an IObservable that can be used with the
// Reactive Extensions
[Get("/users/{user}")]
IObservable<HttpResponseMessage> GetUser(string user);

: "Task";

protected void GenerateObsoleteAttribute(OpenApiOperation operation, StringBuilder code)
{
if (operation.IsDeprecated)
Expand Down
58 changes: 27 additions & 31 deletions src/Refitter.Core/RefitInterfaceImports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,37 @@ namespace Refitter.Core;

internal static class RefitInterfaceImports
{
public static string[] GetImportedNamespaces(RefitGeneratorSettings settings) =>
settings.UseCancellationTokens
? new[]
{
"Refit",
"System.Collections.Generic",
"System.Text.Json.Serialization",
"System.Threading",
"System.Threading.Tasks"
}
: new[]
{
"Refit",
"System.Collections.Generic",
"System.Text.Json.Serialization",
"System.Threading.Tasks"
};
private static string[] defaultNamespases = new[]
{
"Refit",
"System.Collections.Generic",
"System.Text.Json.Serialization",
};
public static string[] GetImportedNamespaces(RefitGeneratorSettings settings)
{
var namespaces = new List<string>(defaultNamespases);
if (settings.UseCancellationTokens)
{
namespaces.Add("System.Threading");
}

if (settings.ReturnIObservable)
{
namespaces.Add("System.Reactive");
christianhelle marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
namespaces.Add("System.Threading.Tasks");
}
return namespaces.ToArray();
}

[SuppressMessage(
"MicrosoftCodeAnalysisCorrectness",
"RS1035:Do not use APIs banned for analyzers",
Justification = "This tool is cross platform")]
public static string GenerateNamespaceImports(RefitGeneratorSettings settings) =>
settings.UseCancellationTokens
? string.Join(
Environment.NewLine,
"using Refit;",
"using System.Collections.Generic;",
"using System.Text.Json.Serialization;",
"using System.Threading;",
"using System.Threading.Tasks;")
: string.Join(
Environment.NewLine,
"using Refit;",
"using System.Collections.Generic;",
"using System.Text.Json.Serialization;",
"using System.Threading.Tasks;");
GetImportedNamespaces(settings)
.Select(ns => $"using {ns};")
.Aggregate((a, b) => $"{a}{Environment.NewLine}{b}");
}
9 changes: 7 additions & 2 deletions src/Refitter.Core/Settings/RefitGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public class RefitGeneratorSettings
/// </summary>
public bool ReturnIApiResponse { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to return IObservable or Task
/// </summary>
public bool ReturnIObservable { get; set; }

/// <summary>
/// Gets or sets a dictionary of operation ids and a specific response type that they should use. The type is
/// wrapped in a task, but otherwise unmodified (so make sure that the namespaces are imported or specified).
Expand Down Expand Up @@ -143,13 +148,13 @@ public class RefitGeneratorSettings
/// Gets or sets the settings describing how to generate types using NSwag
/// </summary>
public CodeGeneratorSettings? CodeGeneratorSettings { get; set; }

/// <summary>
/// Set to <c>true</c> to apply tree-shaking to the OpenApi schema.
/// This works in conjunction with <see cref="IncludeTags"/> and <see cref="IncludePathMatches"/>.
/// </summary>
public bool TrimUnusedSchema { get; set; }

/// <summary>
/// Array of regular expressions that determine if a schema needs to be kept.
/// This works in conjunction with <see cref="TrimUnusedSchema"/>.
Expand Down
19 changes: 19 additions & 0 deletions src/Refitter.Tests/SwaggerPetstoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,25 @@ public async Task Can_Build_Generated_Code_With_IApiResponse(SampleOpenSpecifica
.BeTrue();
}

[Theory]
[InlineData(SampleOpenSpecifications.SwaggerPetstoreJsonV3, "SwaggerPetstore.json")]
public async Task Can_Build_Generated_Code_With_IObservableResponse(SampleOpenSpecifications version, string filename)
{
var settings = new RefitGeneratorSettings();
settings.ReturnIObservable = true;
var generateCode = await GenerateCode(version, filename, settings);
//cannot build without it because System.Reactive package has to be installed first
generateCode += @"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add the System.Reactive package reference to the ProjectFileContents.cs file

<PackageReference Include=""System.Reactive"" Version=""6.0.0"" />

so you don't need to insert a fake Unit type

namespace System.Reactive
{
public class Unit{}
}";
BuildHelper
.BuildCSharp(generateCode)
.Should()
.BeTrue();
}

[Theory]
[InlineData(SampleOpenSpecifications.SwaggerPetstoreJsonV3, "SwaggerPetstore.json")]
#if !DEBUG
Expand Down
5 changes: 3 additions & 2 deletions src/Refitter/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
AddAcceptHeaders = !settings.NoAcceptHeaders,
GenerateContracts = !settings.InterfaceOnly,
ReturnIApiResponse = settings.ReturnIApiResponse,
ReturnIObservable = settings.ReturnIObservable,
UseCancellationTokens = settings.UseCancellationTokens,
GenerateOperationHeaders = !settings.NoOperationHeaders,
UseIsoDateFormat = settings.UseIsoDateFormat,
Expand Down Expand Up @@ -70,7 +71,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
var generator = await RefitGenerator.CreateAsync(refitGeneratorSettings);
if (!settings.SkipValidation)
await ValidateOpenApiSpec(refitGeneratorSettings.OpenApiPath);

var code = generator.Generate().ReplaceLineEndings();
AnsiConsole.MarkupLine($"[green]Length: {code.Length} bytes[/]");

Expand Down Expand Up @@ -99,7 +100,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
AnsiConsole.MarkupLine($"[red]Exception:{Crlf}{exception.GetType()}[/]");
AnsiConsole.MarkupLine($"[yellow]Stack Trace:{Crlf}{exception.StackTrace}[/]");
}

AnsiConsole.MarkupLine("[yellow]#############################################################################[/]");
AnsiConsole.MarkupLine("[yellow]# Consider reporting the problem if you are unable to resolve it yourself #[/]");
AnsiConsole.MarkupLine("[yellow]# https://github.com/christianhelle/refitter/issues #[/]");
Expand Down
11 changes: 8 additions & 3 deletions src/Refitter/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public sealed class Settings : CommandSettings
[DefaultValue(false)]
public bool ReturnIApiResponse { get; set; }

[Description("Return IObservable instead of Task")]
[CommandOption("--use-observable-response")]
[DefaultValue(false)]
public bool ReturnIObservable { get; set; }

[Description("Set the accessibility of the generated types to 'internal'")]
[CommandOption("--internal")]
[DefaultValue(false)]
Expand Down Expand Up @@ -109,7 +114,7 @@ public sealed class Settings : CommandSettings
[CommandOption("--operation-name-template")]
[DefaultValue(null)]
public string? OperationNameTemplate { get; internal set; }

[Description("Generate nullable parameters as optional parameters")]
[CommandOption("--optional-nullable-parameters")]
[DefaultValue(false)]
Expand All @@ -119,7 +124,7 @@ public sealed class Settings : CommandSettings
[CommandOption("--trim-unused-schema")]
[DefaultValue(false)]
public bool TrimUnusedSchema { get; set; }

[Description("Force to keep matching schema, uses regular expressions. Use together with \"--trim-unused-schema\". Can be set multiple times.")]
[CommandOption("--keep-schema")]
[DefaultValue(new string[0])]
Expand All @@ -134,7 +139,7 @@ public sealed class Settings : CommandSettings
[CommandOption("--skip-default-additional-properties")]
[DefaultValue(false)]
public bool SkipDefaultAdditionalProperties { get; set; }

[Description("""
The NSwag IOperationNameGenerator implementation to use.
May be one of:
Expand Down
Loading