RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.76k stars 1.29k forks source link

Any plan to use Refit to generate API clients from OAS? #3027

Open Mubashwer opened 4 years ago

Mubashwer commented 4 years ago

Any plans to use Refit to generate clients from OpenAPI Specifications? Refit is probably the most lightweight REST library in .NET. Only models and interface with attributes need to be generated.

RicoSuter commented 4 years ago

Should be quite simple but i personally do not have plans to add that... but we probably can add that as an additional template.

gabrielmaldi commented 3 years ago

I managed to use NSwag to generate (very opinionated and far from feature-complete) Refit proxies. I'll post the details here in case someone wants to use this code as the base of their own solution which fits their needs.


Refit.liquid

{%- for interface in RefitInterfaces %}
{% template Client.Interface.Annotations %}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "{{ ToolchainVersion }}")]
{{ ClientClassAccessModifier }} partial interface I{{ interface.Name }}{%- if HasClientBaseInterface %} : {{ ClientBaseInterface }}{%- endif %}
{
{%- for operation in interface.Operations -%}
    [Refit.{{ operation.HttpMethodUpper }}("{{ operation.RefitPath }}")]
    {{ operation.ResultType }} {{ operation.RefitOperationName }}({%- for parameter in operation.RefitParameters -%}{%- if parameter.RefitAttribute -%}[Refit.{{ parameter.RefitAttribute }}] {% endif %}{{ parameter.Type }} {{ parameter.RefitName }}{% unless forloop.last %}, {% endunless %}{%- endfor -%});{% unless forloop.last %}
{% endunless %}
{%- endfor -%}
}{% unless forloop.last %}
{% endunless -%}
{%- endfor -%}

Project.csproj

<ItemGroup>
  <EmbeddedResource Include="Templates\*.liquid" />
</ItemGroup>

EmbeddedTemplateFactory.cs

public class EmbeddedTemplateFactory : DefaultTemplateFactory
{
    private static readonly string _SystemTextJsonVersion = $"(System.Text.Json v{typeof(JsonSerializer).Assembly.GetName().Version})";

    public EmbeddedTemplateFactory(CSharpClientGeneratorSettings settings)
        : base(settings.CSharpGeneratorSettings, new[] { settings.GetType().Assembly, settings.CSharpGeneratorSettings.GetType().Assembly })
    {
    }

    protected override string GetToolchainVersion()
    {
        var result = base.GetToolchainVersion();

        // Use System.Text.Json instead of Newtonsoft.Json in <auto-generated> comments.
        // ToDo: remove this when https://github.com/RicoSuter/NJsonSchema/issues/1400 is fixed.
        result = Regex.Replace(result, @"\(Newtonsoft\.Json.*?\)", _SystemTextJsonVersion);

        return result;
    }

    protected override string GetEmbeddedLiquidTemplate(string language, string template)
    {
        string result;

        // Use the overriden or the default liquid template.
        var assembly = Assembly.GetExecutingAssembly();
        var resource = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.Templates.{template}.liquid");
        if (resource != null)
        {
            using var streamReader = new StreamReader(resource);
            result = streamReader.ReadToEnd();
        }
        else
        {
            result = base.GetEmbeddedLiquidTemplate(language, template);
        }

        // Use System.Text.Json instead of Newtonsoft.Json in GeneratedCode attributes.
        // ToDo: remove this when https://github.com/RicoSuter/NJsonSchema/issues/1400 is fixed.
        result = Regex.Replace(result, @"(GeneratedCode\(.*?)\{\{\s*ToolchainVersion\s*\}\}", $"${{1}}{typeof(JsonSchema).Assembly.GetName().Version} {_SystemTextJsonVersion}", RegexOptions.Multiline);

        return result;
    }
}

`RefitGenerator.cs

public class RefitGenerator : CSharpClientGenerator
{
    private readonly OpenApiDocument _document;

    public RefitGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings)
        : base(document, settings)
    {
        _document = document;
    }

    protected override IEnumerable<CodeArtifact> GenerateClientTypes(string controllerName, string controllerClassName, IEnumerable<CSharpOperationModel> operations)
    {
        var exceptionSchema = (Resolver as CSharpTypeResolver)?.ExceptionSchema;
        var model = new RefitTemplateModel(controllerName, controllerClassName, operations, exceptionSchema, _document, Settings);
        var template = Settings.CSharpGeneratorSettings.TemplateFactory.CreateTemplate("CSharp", "Refit", model);

        yield return new CodeArtifact(model.Class, CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Client, template);
    }

    protected override CSharpOperationModel CreateOperationModel(OpenApiOperation operation, ClientGeneratorBaseSettings settings)
    {
        return new RefitOperationModel(operation, (CSharpGeneratorBaseSettings)settings, this, (CSharpTypeResolver)Resolver);
    }
}

RefitModel.cs

public class RefitTemplateModel : CSharpClientTemplateModel
{
    public RefitTemplateModel(string controllerName, string controllerClassName, IEnumerable<CSharpOperationModel> operations, JsonSchema exceptionSchema, OpenApiDocument document, CSharpClientGeneratorSettings settings)
        : base(controllerName, controllerClassName, operations, exceptionSchema, document, settings)
    {
    }

    public IEnumerable<RefitInterfaceModel> RefitInterfaces
    {
        get
        {
            // Currently, all paths are of the form "Service/Public/Controller/Operation/{param}"
            // so we take the third component as the interface name.
            // ToDo: is there a better way to infer this (_document.BaseUrl and _document.BasePath are null)?
            return Operations.GroupBy(operation => operation.Path.Split('/')[2]).Select(group => new RefitInterfaceModel
            {
                Name = group.Key,
                Operations = group
            });
        }
    }
}

public class RefitInterfaceModel
{
    public string Name { get; set; }

    public IEnumerable<CSharpOperationModel> Operations { get; set; }
}

public class RefitOperationModel : CSharpOperationModel
{
    private readonly CSharpGeneratorBaseSettings _settings;
    private readonly CSharpGeneratorBase _generator;
    private readonly CSharpTypeResolver _resolver;

    public RefitOperationModel(OpenApiOperation operation, CSharpGeneratorBaseSettings settings, CSharpGeneratorBase generator, CSharpTypeResolver resolver)
        : base(operation, settings, generator, resolver)
    {
        _settings = settings;
        _generator = generator;
        _resolver = resolver;
    }

    public string RefitOperationName
    {
        get
        {
            var result = OperationName;

            if (result.EndsWith(HttpMethodUpper, StringComparison.InvariantCulture))
            {
                result = result.Substring(0, result.Length - HttpMethodUpper.Length);
            }

            if (HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
            {
                var isArrayResponse = Responses.SingleOrDefault(response => response.StatusCode == "200")?.ActualResponseSchema?.Type.HasFlag(JsonObjectType.Array);
                if (isArrayResponse.GetValueOrDefault())
                {
                    result = "All" + result;
                }
            }

            result = HttpMethodUpper + result + "Async";

            return result;
        }
    }

    public string RefitPath
    {
        get
        {
            // Remove the base URL from the path.
            return '/' + string.Join('/', Path.Split('/').Skip(3));
        }
    }

    public IEnumerable<RefitParameterModel> RefitParameters
    {
        get
        {
            var actualParameters = GetActualParameters();
            var parameters = actualParameters.Select(param => new RefitParameterModel(
                param.Name,
                GetParameterVariableName(param, actualParameters),
                GetParameterVariableIdentifier(param, actualParameters),
                ResolveParameterType(param),
                param,
                actualParameters,
                _settings.CodeGeneratorSettings,
                _generator,
                _resolver
            ));

            return parameters
                .Where(param => !param.IsHeader)
                .OrderBy(param => param.Kind, OpenApiParameterKind.Path, OpenApiParameterKind.Query, OpenApiParameterKind.Body);
        }
    }
}

public class RefitParameterModel : CSharpParameterModel
{
    public RefitParameterModel(string parameterName, string variableName, string variableIdentifier, string typeName, OpenApiParameter parameter, IList<OpenApiParameter> allParameters, CodeGeneratorSettingsBase settings, IClientGenerator generator, TypeResolverBase typeResolver)
        : base(parameterName, variableName, variableIdentifier, typeName, parameter, allParameters, settings, generator, typeResolver)
    {
    }

    public string RefitAttribute
    {
        get
        {
            return Kind switch
            {
                OpenApiParameterKind.Query => "Query",
                OpenApiParameterKind.Body => "Body",
                _ => null,
            };
        }
    }

    public string RefitName
    {
        get
        {
            return Kind switch
            {
                OpenApiParameterKind.Path => Name,
                _ => VariableName,
            };
        }
    }
}

Program.cs

public async Task GenerateProxies()
{
    var swaggerUrl = "https://your-api/swagger/v1.0/swagger.json";
    using var client = new HttpClient();
    var json = await client.GetStringAsync(swaggerUrl);
    var document = await OpenApiDocument.FromJsonAsync(json);
    var settings = new CSharpClientGeneratorSettings
    {
        GenerateClientClasses = true,
        ClientClassAccessModifier = "internal",
        ClientBaseInterface = "ICoreService",
        OperationNameGenerator = new MultipleClientsFromFirstTagAndPathSegmentsOperationNameGenerator(),
        GenerateExceptionClasses = false,
        ParameterArrayType = typeof(IEnumerable<object>).GetFullNameWithoutGenerics(),
        ResponseArrayType = typeof(IEnumerable<object>).GetFullNameWithoutGenerics(),
        CSharpGeneratorSettings =
        {
            SchemaType = SchemaType.OpenApi3,
            JsonLibrary = CSharpJsonLibrary.SystemTextJson,
            Namespace = $"YourNamespace.Proxies.{service}",
            TypeAccessModifier = "internal",
            DateTimeType = typeof(DateTime).FullName,
            ArrayType = typeof(IEnumerable<object>).GetFullNameWithoutGenerics(),
        }
    };
    settings.CSharpGeneratorSettings.TemplateFactory = new EmbeddedTemplateFactory(settings);
    var generator = new RefitGenerator(document, settings);
    var proxy = generator.GenerateFile();

    var utf8NoBom = new UTF8Encoding(false);
    File.WriteAllText("Proxy.cs", proxy, utf8NoBom);
}
wahmedswl commented 7 months ago

https://refitter.github.io/articles/cli-tool.html