diff --git a/AspNetCoreExample.Api/Controllers/WeatherForecastController.cs b/AspNetCoreExample.Api/Controllers/WeatherForecastController.cs index 5cf3a1b..4561b96 100644 --- a/AspNetCoreExample.Api/Controllers/WeatherForecastController.cs +++ b/AspNetCoreExample.Api/Controllers/WeatherForecastController.cs @@ -4,8 +4,6 @@ using Microsoft.AspNetCore.Mvc; -using SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController; - namespace AspNetCoreExample.Api.Controllers; [ApiController] @@ -36,11 +34,11 @@ public void Update(string city, [FromBody] WeatherForecast forecast, Cancellatio } [HttpPost("~/[action]")] - public void Reset(int seed) + public IActionResult Reset(int seed) { + return Ok(); } - [UrlOnly] [HttpGet("{city}")] public ActionResult Download(string city) { @@ -53,6 +51,19 @@ public ActionResult Download(string city) return File(JsonSerializer.SerializeToUtf8Bytes(forecast), "application/json"); } + [HttpGet("{street}/view")] + public async Task GetStreetView(string street, bool useGoogleImages) + { + await Task.Delay(100); + return File(Array.Empty(), "image/jpeg"); + } + + [HttpGet("none")] + public Task> NewGuid() + { + return Task.FromResult((ActionResult)Ok(Guid.NewGuid())); + } + private static readonly string[] summaries = { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" diff --git a/AspNetCoreExample.Generator/output/api/NotesApi.ts b/AspNetCoreExample.Generator/output/api/NotesApi.ts index ec3b315..db65a53 100644 --- a/AspNetCoreExample.Generator/output/api/NotesApi.ts +++ b/AspNetCoreExample.Generator/output/api/NotesApi.ts @@ -3,20 +3,15 @@ import { Guid } from './../DataTypes/Guid'; import { BlogEntry } from './../DataTypes/BlogEntry'; import { ApiBase } from './../ApiBase/ApiBase'; +import { url } from './../ApiBase/ApiBase'; export class NotesApi extends ApiBase implements INotesApi { - async addEntry(userId: Guid, entry: BlogEntry): Promise { - return this.makePostRequest(`/v1/user/${userId}/blog`, { - - }, { - ...entry, - }); + addEntry(userId: Guid, entry: BlogEntry): Promise { + return this.makePostRequest(url`/v1/user/${userId}/blog`, entry); } - async addEntries(userId: Guid, entries: BlogEntry[]): Promise { - return this.makePostRequest(`/v1/user/${userId}/blog/batch`, { - - }, entries); + addEntries(userId: Guid, entries: BlogEntry[]): Promise { + return this.makePostRequest(url`/v1/user/${userId}/blog/batch`, entries); } }; diff --git a/AspNetCoreExample.Generator/output/api/UserApi.ts b/AspNetCoreExample.Generator/output/api/UserApi.ts index bab0e5c..b62c2b2 100644 --- a/AspNetCoreExample.Generator/output/api/UserApi.ts +++ b/AspNetCoreExample.Generator/output/api/UserApi.ts @@ -3,38 +3,23 @@ import { User } from './../DataTypes/User'; import { Guid } from './../DataTypes/Guid'; import { ApiBase } from './../ApiBase/ApiBase'; +import { url } from './../ApiBase/ApiBase'; export class UserApi extends ApiBase implements IUserApi { - async createUser(user: User): Promise { - return this.makePostRequest(`/v1/users`, { - - }, { - ...user, - }); + createUser(user: User): Promise { + return this.makePostRequest(url`/v1/users`, user); } - async deleteUser(userId: Guid): Promise { - return this.makeDeleteRequest(`/v1/users/${userId}`, { - - }, { - - }); + deleteUser(userId: Guid): Promise { + return this.makeDeleteRequest(url`/v1/users/${userId}`); } - async getUser(userId: Guid): Promise { - return this.makeGetRequest(`/v1/users/${userId}`, { - - }, { - - }); + getUser(userId: Guid): Promise { + return this.makeGetRequest(url`/v1/users/${userId}`); } - async searchUsers(name: string): Promise { - return this.makeGetRequest(`/v1/users`, { - ['name']: name, - }, { - - }); + searchUsers(name: string): Promise { + return this.makeGetRequest(url`/v1/users?name=${name}`); } }; diff --git a/AspNetCoreExample.Generator/output/api/WeatherForecastApi.ts b/AspNetCoreExample.Generator/output/api/WeatherForecastApi.ts index 52ca0c4..f7caa54 100644 --- a/AspNetCoreExample.Generator/output/api/WeatherForecastApi.ts +++ b/AspNetCoreExample.Generator/output/api/WeatherForecastApi.ts @@ -1,35 +1,33 @@ // tslint:disable // TypeScriptContractGenerator's generated content import { WeatherForecast } from './../DataTypes/WeatherForecast'; +import { Guid } from './../DataTypes/Guid'; import { ApiBase } from './../ApiBase/ApiBase'; +import { url } from './../ApiBase/ApiBase'; export class WeatherForecastApi extends ApiBase implements IWeatherForecastApi { - async get(): Promise { - return this.makeGetRequest(`/WeatherForecast`, { - - }, { - - }); + get(): Promise { + return this.makeGetRequest(url`/WeatherForecast`); } - async update(city: string, forecast: WeatherForecast): Promise { - return this.makePostRequest(`/WeatherForecast/Update/${city}`, { - - }, { - ...forecast, - }); + update(city: string, forecast: WeatherForecast): Promise { + return this.makePostRequest(url`/WeatherForecast/Update/${city}`, forecast); } - async reset(seed: number): Promise { - return this.makePostRequest(`/Reset`, { - ['seed']: seed, - }, { - - }); + reset(seed: number): Promise { + return this.makePostRequest(url`/Reset?seed=${seed}`); } - getDownloadUrl(city: string): string { - return `/WeatherForecast/${city}`; + urlForDownload(city: string): string { + return url`/WeatherForecast/${city}`; + } + + urlForGetStreetView(street: string, useGoogleImages: boolean): string { + return url`/WeatherForecast/${street}/view?useGoogleImages=${useGoogleImages}`; + } + + newGuid(): Promise { + return this.makeGetRequest(url`/WeatherForecast/none`); } }; @@ -37,5 +35,7 @@ export interface IWeatherForecastApi { get(): Promise; update(city: string, forecast: WeatherForecast): Promise; reset(seed: number): Promise; - getDownloadUrl(city: string): string; + urlForDownload(city: string): string; + urlForGetStreetView(street: string, useGoogleImages: boolean): string; + newGuid(): Promise; } diff --git a/AspNetCoreExample.Generator/output/apiBase/ApiBase.ts b/AspNetCoreExample.Generator/output/apiBase/ApiBase.ts index 8e0beb4..74a9f03 100644 --- a/AspNetCoreExample.Generator/output/apiBase/ApiBase.ts +++ b/AspNetCoreExample.Generator/output/apiBase/ApiBase.ts @@ -1,21 +1,19 @@ /* eslint-disable */ -interface IParamsMap { - [key: string]: null | undefined | number | string | any[] | boolean; -} +export const url = String.raw; export class ApiBase { - public async makeGetRequest(url: string, queryParams: IParamsMap, body: any): Promise { - const response = await fetch(this.getUrl(url, queryParams), { + public async makeGetRequest(url: string, body?: any): Promise { + const response = await fetch(url, { method: "GET", }); return await response.json(); } - public async makePostRequest(url: string, queryParams: IParamsMap, body: any): Promise { - const response = await fetch(this.getUrl(url, queryParams), { + public async makePostRequest(url: string, body?: any): Promise { + const response = await fetch(url, { method: "POST", - body: JSON.stringify(body), + body: body && JSON.stringify(body), }); const textResult = await response.text(); if (textResult !== "") { @@ -23,10 +21,10 @@ export class ApiBase { } } - public async makePutRequest(url: string, queryParams: IParamsMap, body: any): Promise { - const response = await fetch(this.getUrl(url, queryParams), { + public async makePutRequest(url: string, body?: any): Promise { + const response = await fetch(url, { method: "PUT", - body: JSON.stringify(body), + body: body && JSON.stringify(body), }); const textResult = await response.text(); if (textResult !== "") { @@ -34,25 +32,14 @@ export class ApiBase { } } - public async makeDeleteRequest(url: string, queryParams: IParamsMap, body: any): Promise { - const response = await fetch(this.getUrl(url, queryParams), { + public async makeDeleteRequest(url: string, body?: any): Promise { + const response = await fetch(url, { method: "DELETE", - body: JSON.stringify(body), + body: body && JSON.stringify(body), }); const textResult = await response.text(); if (textResult !== "") { return JSON.parse(textResult); } } - - public getUrl(url: string, params?: IParamsMap): string { - return url + this.createQueryString(params); - } - - public createQueryString(params: any): string { - if (params == null) { - return ""; - } - return `${new URLSearchParams(params)}`; - } } diff --git a/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.Expected.txt b/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.Expected.txt index c7a5205..93cca8f 100644 --- a/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.Expected.txt +++ b/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.Expected.txt @@ -6,11 +6,11 @@ using SkbKontur.TypeScript.ContractGenerator; using SkbKontur.TypeScript.ContractGenerator.Abstractions; using SkbKontur.TypeScript.ContractGenerator.Extensions; using SkbKontur.TypeScript.ContractGenerator.Internals; -using SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController; +using SkbKontur.TypeScript.ContractGenerator.TypeBuilders; namespace AspNetCoreExample.Generator { - public class ApiControllerTypeBuildingContext : ApiControllerTypeBuildingContextBase + public class ApiControllerTypeBuildingContext : TypeBuildingContext { public ApiControllerTypeBuildingContext(TypeScriptUnit unit, ITypeInfo type) : base(unit, type) @@ -29,7 +29,7 @@ namespace AspNetCoreExample.Generator return SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[6].IsAssignableFrom(type); } - protected override TypeLocation GetApiBase(ITypeInfo controllerType) + protected TypeLocation GetApiBase(ITypeInfo controllerType) { var apiBaseName = GetApiBaseName(controllerType); return new TypeLocation @@ -46,7 +46,7 @@ namespace AspNetCoreExample.Generator return "ApiBase"; } - protected override ITypeInfo ResolveReturnType(ITypeInfo typeInfo) + protected ITypeInfo ResolveReturnType(ITypeInfo typeInfo) { if (typeInfo.IsGenericType) { @@ -62,24 +62,24 @@ namespace AspNetCoreExample.Generator return typeInfo; } - protected override BaseApiMethod ResolveBaseApiMethod(IMethodInfo methodInfo) + protected string ResolveBaseApiMethod(IMethodInfo methodInfo) { if (methodInfo.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[13]).Any()) - return BaseApiMethod.Get; + return "Get"; if (methodInfo.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[14]).Any()) - return BaseApiMethod.Post; + return "Post"; if (methodInfo.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[15]).Any()) - return BaseApiMethod.Put; + return "Put"; if (methodInfo.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[16]).Any()) - return BaseApiMethod.Delete; + return "Delete"; throw new NotSupportedException($"Unresolved http verb for method {methodInfo.Name} at controller {methodInfo.DeclaringType?.Name}"); } - protected override string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo) + protected string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo) { var routeTemplate = methodInfo.GetAttributes(false) .Select(x => x.AttributeData.TryGetValue("Template", out var value) ? (string)value : null) @@ -87,24 +87,24 @@ namespace AspNetCoreExample.Generator return AppendRoutePrefix(routeTemplate, controllerType); } - protected override bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) + protected bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) { if (IsUserScopedApi(controllerType) && parameterInfo.Name == "userId") return false; return true; } - protected override IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType) + protected IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType) { return parameters.Where(x => PassParameterToCall(x, controllerType) && !x.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[17]).Any()).ToArray(); } - protected override IParameterInfo GetBody(IParameterInfo[] parameters, ITypeInfo controllerType) + protected IParameterInfo GetBody(IParameterInfo[] parameters, ITypeInfo controllerType) { return parameters.SingleOrDefault(x => PassParameterToCall(x, controllerType) && x.GetAttributes(SkbKontur.TypeScript.ContractGenerator.Roslyn.TypeInfoRewriter.Types[18]).Any()); } - protected override IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType) + protected IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType) { return controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public) .Where(m => !((MethodWrapper)m).Method.IsSpecialName) diff --git a/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.txt b/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.txt index 64ccedf..8bf6581 100644 --- a/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.txt +++ b/TypeScript.ContractGenerator.Tests/Files/ApiControllerTypeBuildingContext.txt @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Reflection; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -9,7 +8,7 @@ using SkbKontur.TypeScript.ContractGenerator; using SkbKontur.TypeScript.ContractGenerator.Abstractions; using SkbKontur.TypeScript.ContractGenerator.Extensions; using SkbKontur.TypeScript.ContractGenerator.Internals; -using SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController; +using SkbKontur.TypeScript.ContractGenerator.TypeBuilders; using static SkbKontur.TypeScript.ContractGenerator.Internals.TypeInfo; @@ -18,7 +17,7 @@ using AnotherTypeInfo = SkbKontur.TypeScript.ContractGenerator.Internals.TypeInf namespace AspNetCoreExample.Generator { - public class ApiControllerTypeBuildingContext : ApiControllerTypeBuildingContextBase + public class ApiControllerTypeBuildingContext : TypeBuildingContext { public ApiControllerTypeBuildingContext(TypeScriptUnit unit, ITypeInfo type) : base(unit, type) @@ -37,7 +36,7 @@ namespace AspNetCoreExample.Generator return TypeInfo.From().IsAssignableFrom(type); } - protected override TypeLocation GetApiBase(ITypeInfo controllerType) + protected TypeLocation GetApiBase(ITypeInfo controllerType) { var apiBaseName = GetApiBaseName(controllerType); return new TypeLocation @@ -54,7 +53,7 @@ namespace AspNetCoreExample.Generator return "ApiBase"; } - protected override ITypeInfo ResolveReturnType(ITypeInfo typeInfo) + protected ITypeInfo ResolveReturnType(ITypeInfo typeInfo) { if (typeInfo.IsGenericType) { @@ -70,24 +69,24 @@ namespace AspNetCoreExample.Generator return typeInfo; } - protected override BaseApiMethod ResolveBaseApiMethod(IMethodInfo methodInfo) + protected string ResolveBaseApiMethod(IMethodInfo methodInfo) { if (methodInfo.GetAttributes(TypeInfo.From()).Any()) - return BaseApiMethod.Get; + return "Get"; if (methodInfo.GetAttributes(TypeInfo.From()).Any()) - return BaseApiMethod.Post; + return "Post"; if (methodInfo.GetAttributes(TypeInfo.From()).Any()) - return BaseApiMethod.Put; + return "Put"; if (methodInfo.GetAttributes(TypeInfo.From()).Any()) - return BaseApiMethod.Delete; + return "Delete"; throw new NotSupportedException($"Unresolved http verb for method {methodInfo.Name} at controller {methodInfo.DeclaringType?.Name}"); } - protected override string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo) + protected string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo) { var routeTemplate = methodInfo.GetAttributes(false) .Select(x => x.AttributeData.TryGetValue("Template", out var value) ? (string)value : null) @@ -95,24 +94,24 @@ namespace AspNetCoreExample.Generator return AppendRoutePrefix(routeTemplate, controllerType); } - protected override bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) + protected bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) { if (IsUserScopedApi(controllerType) && parameterInfo.Name == "userId") return false; return true; } - protected override IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType) + protected IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType) { return parameters.Where(x => PassParameterToCall(x, controllerType) && !x.GetAttributes(TypeInfo.From()).Any()).ToArray(); } - protected override IParameterInfo GetBody(IParameterInfo[] parameters, ITypeInfo controllerType) + protected IParameterInfo GetBody(IParameterInfo[] parameters, ITypeInfo controllerType) { return parameters.SingleOrDefault(x => PassParameterToCall(x, controllerType) && x.GetAttributes(TypeInfo.From()).Any()); } - protected override IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType) + protected IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType) { return controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public) .Where(m => !((MethodWrapper)m).Method.IsSpecialName) diff --git a/TypeScript.ContractGenerator.Tests/OptionsTests.cs b/TypeScript.ContractGenerator.Tests/OptionsTests.cs index 758c5db..e8c489d 100644 --- a/TypeScript.ContractGenerator.Tests/OptionsTests.cs +++ b/TypeScript.ContractGenerator.Tests/OptionsTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Text.RegularExpressions; using NUnit.Framework; @@ -23,6 +24,12 @@ public void OptionalPropertiesTest(bool optionalPropertiesEnabled, string expect generatedCode.Diff(expectedCode).ShouldBeEmpty(); } + [Test] + public void v() + { + var prms = Regex.Matches("/v1/user/{userId}/blog", @"{(\w+)}"); + } + [TestCase(NullabilityMode.Pessimistic, "explicit-nullability-enabled")] [TestCase(NullabilityMode.None, "explicit-nullability-disabled")] public void ExplicitNullabilityTest(NullabilityMode nullabilityMode, string expectedFileName) diff --git a/TypeScript.ContractGenerator/CodeDom/TypeScriptTemplateStringLiteral.cs b/TypeScript.ContractGenerator/CodeDom/TypeScriptTemplateStringLiteral.cs index bc1040f..1c1ceeb 100644 --- a/TypeScript.ContractGenerator/CodeDom/TypeScriptTemplateStringLiteral.cs +++ b/TypeScript.ContractGenerator/CodeDom/TypeScriptTemplateStringLiteral.cs @@ -2,16 +2,19 @@ namespace SkbKontur.TypeScript.ContractGenerator.CodeDom { public class TypeScriptTemplateStringLiteral : TypeScriptExpression { - public TypeScriptTemplateStringLiteral(string value) + public TypeScriptTemplateStringLiteral(string value, TypeScriptVariableReference? tag = null) { Value = value; + Tag = tag; } + public TypeScriptVariableReference? Tag { get; } public string Value { get; } public override string GenerateCode(ICodeGenerationContext context) { - return $"`{Value}`"; + var tag = Tag == null ? string.Empty : Tag.GenerateCode(context); + return $"{tag}`{Value}`"; } } } \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContext.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContext.cs index 81e7af8..ab1d326 100644 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContext.cs +++ b/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContext.cs @@ -1,138 +1,140 @@ using System; +using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; using SkbKontur.TypeScript.ContractGenerator.Abstractions; using SkbKontur.TypeScript.ContractGenerator.CodeDom; -using SkbKontur.TypeScript.ContractGenerator.Extensions; - -using TypeInfo = SkbKontur.TypeScript.ContractGenerator.Internals.TypeInfo; namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController { - public class ApiControllerTypeBuildingContext : ApiControllerTypeBuildingContextBase + public class ApiControllerTypeBuildingContext : TypeBuildingContext { - public ApiControllerTypeBuildingContext(TypeScriptUnit unit, ITypeInfo type) + public ApiControllerTypeBuildingContext(TypeScriptUnit unit, ITypeInfo type, IApiCustomization? apiCustomization = null) : base(unit, type) { + ApiCustomization = apiCustomization ?? new DefaultApiCustomization(); } - protected override TypeLocation GetApiBase(ITypeInfo controllerType) - { - return new TypeLocation - { - Name = "ApiBase", - Location = "ApiBase/ApiBase", - }; - } + protected IApiCustomization ApiCustomization { get; } + protected Func BuildAndImportType { get; private set; } - protected override ITypeInfo ResolveReturnType(ITypeInfo typeInfo) + public override void Initialize(ITypeGenerator typeGenerator) { - if (typeInfo.IsGenericType) - { - var genericTypeDefinition = typeInfo.GetGenericTypeDefinition(); - if (genericTypeDefinition.Equals(TypeInfo.From(typeof(Task<>))) || - genericTypeDefinition.Name == KnownTypeNames.ActionResultOfT || - genericTypeDefinition.Name == KnownTypeNames.ActionResult) - return ResolveReturnType(typeInfo.GetGenericArguments()[0]); - } - - if (typeInfo.Equals(TypeInfo.From()) || typeInfo.Name == KnownTypeNames.ActionResult) - return TypeInfo.From(typeof(void)); + BuildAndImportType = t => typeGenerator.BuildAndImportType(Unit, t); - return typeInfo; - } - - protected override BaseApiMethod ResolveBaseApiMethod(IMethodInfo methodInfo) - { - var attributes = methodInfo.GetAttributes(inherit : false); + var baseApi = ApiCustomization.GetApiBase(Type); + var urlTag = ApiCustomization.GetUrlTag(Type); - if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpGet))) - return BaseApiMethod.Get; + Unit.AddSymbolImport(baseApi.Name, baseApi.Location); + Unit.AddSymbolImport(urlTag.Name, urlTag.Location); - if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPost))) - return BaseApiMethod.Post; + var apiName = ApiCustomization.GetApiClassName(Type); + var interfaceName = ApiCustomization.GetApiInterfaceName(Type); + var apiMethods = ApiCustomization.GetApiMethods(Type); - if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPut))) - return BaseApiMethod.Put; + var apiInterfaceDefinition = new TypeScriptInterfaceDefinition(); + apiInterfaceDefinition.Members.AddRange(apiMethods.SelectMany(BuildApiInterfaceMember)); - if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpDelete))) - return BaseApiMethod.Delete; - - if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPatch))) - return BaseApiMethod.Patch; - - throw new NotSupportedException( - $"Unresolved http verb for method {methodInfo.Name} at controller {methodInfo.DeclaringType?.Name}"); - } - - protected override string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo) - { - var fullRoute = RouteTemplateHelper.FindFullRouteTemplate(controllerType, methodInfo)?.ValueWithoutConstraints ?? ""; - if (fullRoute.StartsWith("/")) - return fullRoute; + var apiClassDefinition = new TypeScriptClassDefinition + { + BaseClass = new TypeScriptTypeReference(baseApi.Name), + ImplementedInterfaces = new TypeScriptType[] {new TypeScriptTypeReference(interfaceName)}, + }; - return "/" + fullRoute; - } + apiClassDefinition.Members.AddRange(apiMethods.SelectMany(BuildApiImplMember)); - protected override IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType) - { - return parameters.Where(x => PassParameterToCall(x, controllerType) && !IsFromBody(x)) - .ToArray(); - } + Unit.Body.Add(new TypeScriptExportStatement + { + Declaration = new TypeScriptClassDeclaration + { + Name = apiName, + Defintion = apiClassDefinition + } + }); + + foreach (var statement in BuildAdditionalStatements()) + { + Unit.Body.Add(statement); + } - protected override IParameterInfo? GetBody(IParameterInfo[] parameters, ITypeInfo controllerType) - { - return parameters.SingleOrDefault(x => PassParameterToCall(x, controllerType) && IsFromBody(x)); - } + Declaration = new TypeScriptInterfaceDeclaration + { + Name = interfaceName, + Definition = apiInterfaceDefinition + }; - protected override IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType) - { - return controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) - .Where(m => m.GetAttributes(inherit : false) - .Any(a => KnownTypeNames.HttpAttributeNames.Any(a.HasName))) - .ToArray(); + base.Initialize(typeGenerator); } - protected override bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) + protected virtual IEnumerable BuildApiImplMember(IMethodInfo methodInfo) { - return !parameterInfo.ParameterType.Equals(TypeInfo.From()); - } + var functionDefinition = new TypeScriptFunctionDefinition + { + IsAsync = ApiCustomization.IsAsyncMethod(methodInfo), + Result = ApiCustomization.GetMethodResultType(methodInfo, BuildAndImportType), + Body = {CreateCall(methodInfo)} + }; - protected override string GetMethodName(IMethodInfo methodInfo) - { - return IsUrlMethod(methodInfo) ? $"get{methodInfo.Name}Url" : methodInfo.Name.ToLowerCamelCase(); + functionDefinition.Arguments.AddRange( + ApiCustomization + .GetMethodParameters(methodInfo) + .Select(x => new TypeScriptArgumentDeclaration + { + Name = x.Name, + Type = BuildAndImportType(x.ParameterType) + }) + ); + + yield return new TypeScriptClassMemberDefinition + { + Name = ApiCustomization.GetMethodName(methodInfo), + Definition = functionDefinition + }; } - protected override TypeScriptReturnStatement CreateCall(IMethodInfo methodInfo, ITypeInfo controllerType) + protected virtual TypeScriptReturnStatement CreateCall(IMethodInfo methodInfo) { - if (!IsUrlMethod(methodInfo)) - return base.CreateCall(methodInfo, controllerType); + var route = ApiCustomization.GetMethodRoute(methodInfo); + var routeExpression = new TypeScriptTemplateStringLiteral(route.Replace("{", "${"), new TypeScriptVariableReference("url")); - var routeTemplate = BuildRoute(controllerType, methodInfo); - return new TypeScriptReturnStatement(new TypeScriptTemplateStringLiteral(routeTemplate.Replace("{", "${"))); - } + if (ApiCustomization.IsUrlMethod(methodInfo)) + { + return new TypeScriptReturnStatement(routeExpression); + } - protected override bool IsAsyncMethod(IMethodInfo methodInfo) - { - return !IsUrlMethod(methodInfo); - } + var bodyExpression = ApiCustomization.GetMethodBodyExpression(methodInfo); + var arguments = bodyExpression == null + ? new[] {routeExpression} + : new[] {routeExpression, bodyExpression}; - protected override TypeScriptType? ResolveReturnType(IMethodInfo methodInfo, Func buildAndImportType) - { - return IsUrlMethod(methodInfo) ? new TypeScriptTypeReference("string") : null; + return new TypeScriptReturnStatement( + new TypeScriptMethodCallExpression(new TypeScriptThisReference(), ApiCustomization.GetMethodVerb(methodInfo), arguments) + ); } - private static bool IsFromBody(IParameterInfo parameterInfo) + protected virtual IEnumerable BuildApiInterfaceMember(IMethodInfo methodInfo) { - return parameterInfo.GetAttributes(inherit : false).Any(x => x.HasName(KnownTypeNames.Attributes.FromBody)); + var result = new TypeScriptInterfaceFunctionMember( + ApiCustomization.GetMethodName(methodInfo), + ApiCustomization.GetMethodResultType(methodInfo, BuildAndImportType) + ); + + result.Arguments.AddRange( + ApiCustomization + .GetMethodParameters(methodInfo) + .Select(x => new TypeScriptArgumentDeclaration + { + Name = x.Name, + Type = BuildAndImportType(x.ParameterType) + }) + ); + + yield return result; } - private static bool IsUrlMethod(IMethodInfo methodInfo) + protected virtual IEnumerable BuildAdditionalStatements() { - return methodInfo.GetAttributes(TypeInfo.From()).Any(); + yield break; } } } \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContextBase.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContextBase.cs deleted file mode 100644 index 0da2a77..0000000 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/ApiControllerTypeBuildingContextBase.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -using SkbKontur.TypeScript.ContractGenerator.Abstractions; -using SkbKontur.TypeScript.ContractGenerator.CodeDom; -using SkbKontur.TypeScript.ContractGenerator.Extensions; -using SkbKontur.TypeScript.ContractGenerator.Internals; - -namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController -{ - public abstract class ApiControllerTypeBuildingContextBase : TypeBuildingContext - { - public ApiControllerTypeBuildingContextBase(TypeScriptUnit unit, ITypeInfo type) - : base(unit, type) - { - } - - protected virtual TypeLocation GetApiBase(ITypeInfo controllerType) - { - return new TypeLocation - { - Name = "ApiBase", - Location = "../apiBase/ApiBase", - }; - } - - protected virtual string GetApiName(ITypeInfo controllerType) - { - return new Regex("(Api)?Controller").Replace(controllerType.Name, "Api"); - } - - protected virtual ITypeInfo ResolveReturnType(ITypeInfo typeInfo) - { - if (typeInfo.IsGenericType) - { - var genericTypeDefinition = typeInfo.GetGenericTypeDefinition(); - if (genericTypeDefinition.Equals(TypeInfo.From(typeof(Task<>)))) - return ResolveReturnType(typeInfo.GetGenericArguments()[0]); - } - - if (typeInfo.Equals(TypeInfo.From())) - return TypeInfo.From(typeof(void)); - return typeInfo; - } - - protected virtual TypeScriptType? ResolveReturnType(IMethodInfo methodInfo, Func buildAndImportType) - { - return null; - } - - protected virtual string GetMethodName(IMethodInfo methodInfo) - { - return methodInfo.Name.ToLowerCamelCase(); - } - - protected virtual bool IsAsyncMethod(IMethodInfo methodInfo) - { - return true; - } - - protected virtual bool PassParameterToCall(IParameterInfo parameterInfo, ITypeInfo controllerType) => true; - - protected virtual TypeScriptStatement WrapCall(IMethodInfo methodInfo, TypeScriptReturnStatement call) => call; - - protected virtual string GetApiClassName(string apiName) => apiName; - - protected abstract BaseApiMethod ResolveBaseApiMethod(IMethodInfo methodInfo); - protected abstract string BuildRoute(ITypeInfo controllerType, IMethodInfo methodInfo); - protected abstract IParameterInfo[] GetQueryParameters(IParameterInfo[] parameters, ITypeInfo controllerType); - protected abstract IParameterInfo? GetBody(IParameterInfo[] parameters, ITypeInfo controllerType); - protected abstract IMethodInfo[] GetMethodsToImplement(ITypeInfo controllerType); - - public override void Initialize(ITypeGenerator typeGenerator) - { - Declaration = GenerateInternalApiController(Unit, Type, x => typeGenerator.BuildAndImportType(Unit, x)); - base.Initialize(typeGenerator); - } - - private TypeScriptTypeDeclaration GenerateInternalApiController(TypeScriptUnit targetUnit, ITypeInfo type, Func buildAndImportType) - { - var baseApi = GetApiBase(type); - var apiName = GetApiName(type); - var interfaceName = "I" + apiName; - var methodInfos = GetMethodsToImplement(type); - - var definition = new TypeScriptInterfaceDefinition(); - definition.Members.AddRange(methodInfos - .SelectMany(x => BuildApiInterfaceMember(x, buildAndImportType, type))); - targetUnit.AddSymbolImport(baseApi.Name, baseApi.Location); - - var interfaceDeclaration = new TypeScriptInterfaceDeclaration - { - Name = interfaceName, - Definition = definition - }; - var typeScriptClassDefinition = new TypeScriptClassDefinition - { - BaseClass = new TypeScriptTypeReference(baseApi.Name), - ImplementedInterfaces = new TypeScriptType[] {new TypeScriptTypeReference(interfaceName)}, - }; - - typeScriptClassDefinition.Members.AddRange( - methodInfos - .SelectMany(x => BuildApiImplMember(x, buildAndImportType, type))); - - targetUnit.Body.Add(new TypeScriptExportStatement - { - Declaration = new TypeScriptClassDeclaration - { - Name = GetApiClassName(apiName), - Defintion = typeScriptClassDefinition - } - }); - - return interfaceDeclaration; - } - - protected virtual IEnumerable BuildApiImplMember(IMethodInfo methodInfo, Func buildAndImportType, ITypeInfo controllerType) - { - var functionDefinition = new TypeScriptFunctionDefinition - { - IsAsync = IsAsyncMethod(methodInfo), - Result = GetMethodResult(methodInfo, buildAndImportType), - Body = {WrapCall(methodInfo, CreateCall(methodInfo, controllerType))} - }; - functionDefinition.Arguments.AddRange( - methodInfo.GetParameters().Where(x => PassParameterToCall(x, controllerType)).Select(x => new TypeScriptArgumentDeclaration - { - Name = x.Name, - Type = buildAndImportType(x.ParameterType) - }) - ); - yield return - new TypeScriptClassMemberDefinition - { - Name = GetMethodName(methodInfo), - Definition = functionDefinition - }; - } - - private TypeScriptType GetMethodResult(IMethodInfo methodInfo, Func buildAndImportType) - { - return ResolveReturnType(methodInfo, buildAndImportType) ?? new TypeScriptPromiseOfType(buildAndImportType(ResolveReturnType(methodInfo.ReturnType))); - } - - protected virtual TypeScriptReturnStatement CreateCall(IMethodInfo methodInfo, ITypeInfo controllerType) - { - var verb = ResolveBaseApiMethod(methodInfo); - return GenerateMethodCallWithBody(methodInfo, $"make{verb}Request", controllerType); - } - - private TypeScriptReturnStatement GenerateMethodCallWithBody(IMethodInfo methodInfo, string methodName, ITypeInfo controllerType) - { - var route = BuildRoute(controllerType, methodInfo); - return new TypeScriptReturnStatement( - new TypeScriptMethodCallExpression( - new TypeScriptThisReference(), - methodName, - new TypeScriptTemplateStringLiteral(route.Replace("{", "${")), - GenerateConstructGetParams( - GetQueryParameters(methodInfo.GetParameters(), controllerType), - route - ), - GetBodyExpression(methodInfo, methodName, controllerType) - )); - } - - private TypeScriptExpression GetBodyExpression(IMethodInfo methodInfo, string methodName, ITypeInfo controllerType) - { - return GenerateCustomBody(methodInfo, methodName, controllerType) ?? - GenerateConstructBody(GetBody(methodInfo.GetParameters(), controllerType)); - } - - private static TypeScriptExpression GenerateConstructBody(IParameterInfo? parameter) - { - if (parameter == null) - return new TypeScriptObjectLiteral(); - - if (parameter.ParameterType.IsArray) - return new TypeScriptVariableReference(parameter.Name); - - return new TypeScriptObjectLiteral( - new TypeScriptObjectLiteralSpread(new TypeScriptVariableReference(parameter.Name)) - ); - } - - protected virtual TypeScriptExpression? GenerateCustomBody(IMethodInfo methodInfo, string methodName, ITypeInfo controllerType) - { - return null; - } - - private static TypeScriptExpression GenerateConstructGetParams(IParameterInfo[] parameters, string routeTemplate) - { - var literalProperties = parameters - .Where(x => !routeTemplate.Contains("{" + x.Name + "}")) - .Select(parameter => (TypeScriptObjectLiteralInitializer)new TypeScriptObjectLiteralProperty( - new TypeScriptStringLiteral(parameter.Name), - new TypeScriptVariableReference(parameter.Name))) - .ToArray(); - var result = new TypeScriptObjectLiteral(literalProperties); - return result; - } - - protected virtual IEnumerable BuildApiInterfaceMember(IMethodInfo methodInfo, Func buildAndImportType, ITypeInfo controllerType) - { - var result = new TypeScriptInterfaceFunctionMember( - GetMethodName(methodInfo), - GetMethodResult(methodInfo, buildAndImportType) - ); - result.Arguments.AddRange( - methodInfo.GetParameters() - .Where(x => PassParameterToCall(x, controllerType)) - .Select(x => new TypeScriptArgumentDeclaration - { - Name = x.Name, - Type = buildAndImportType(x.ParameterType) - }) - ); - yield return result; - } - } -} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/BaseApiMethod.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/BaseApiMethod.cs deleted file mode 100644 index 4e74232..0000000 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/BaseApiMethod.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController -{ - public enum BaseApiMethod - { - Get, - Post, - Put, - Delete, - Download, - Upload, - Patch - } -} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/DefaultApiCustomization.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/DefaultApiCustomization.cs new file mode 100644 index 0000000..a94d583 --- /dev/null +++ b/TypeScript.ContractGenerator/TypeBuilders/ApiController/DefaultApiCustomization.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using SkbKontur.TypeScript.ContractGenerator.Abstractions; +using SkbKontur.TypeScript.ContractGenerator.CodeDom; +using SkbKontur.TypeScript.ContractGenerator.Extensions; + +using TypeInfo = SkbKontur.TypeScript.ContractGenerator.Internals.TypeInfo; + +namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController +{ + public class DefaultApiCustomization : IApiCustomization + { + public TypeLocation GetApiBase(ITypeInfo type) => new TypeLocation + { + Name = "ApiBase", + Location = "ApiBase/ApiBase", + }; + + public TypeLocation GetUrlTag(ITypeInfo type) => new TypeLocation + { + Name = "url", + Location = "ApiBase/ApiBase", + }; + + public string GetApiClassName(ITypeInfo type) => new Regex("(Api)?Controller").Replace(type.Name, "Api"); + public string GetApiInterfaceName(ITypeInfo type) => "I" + GetApiClassName(type); + + public string GetMethodName(IMethodInfo methodInfo) => IsUrlMethod(methodInfo) + ? $"urlFor{methodInfo.Name}" + : methodInfo.Name.ToLowerCamelCase(); + + public IMethodInfo[] GetApiMethods(ITypeInfo type) => + type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .Where(m => m.GetAttributes(inherit : false).Any(a => KnownTypeNames.HttpAttributeNames.Any(a.HasName))) + .ToArray(); + + public IParameterInfo[] GetMethodParameters(IMethodInfo methodInfo) => + methodInfo.GetParameters() + .Where(x => !x.ParameterType.Equals(TypeInfo.From())) + .ToArray(); + + public bool IsUrlMethod(IMethodInfo methodInfo) + { + return GetMethodVerb(methodInfo) == "makeGetRequest" && ResolveReturnType(methodInfo.ReturnType).Equals(TypeInfo.From(typeof(void))); + } + + public bool IsAsyncMethod(IMethodInfo methodInfo) => false; + + public string GetMethodVerb(IMethodInfo methodInfo) + { + var attributes = methodInfo.GetAttributes(inherit : false); + + if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpGet))) + return "makeGetRequest"; + + if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPost))) + return "makePostRequest"; + + if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPut))) + return "makePutRequest"; + + if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpDelete))) + return "makeDeleteRequest"; + + if (attributes.Any(x => x.HasName(KnownTypeNames.Attributes.HttpPatch))) + return "makePatchRequest"; + + throw new NotSupportedException($"Unresolved http verb for method {methodInfo.Name} at controller {methodInfo.DeclaringType?.Name}"); + } + + public string GetMethodRoute(IMethodInfo methodInfo) + { + var path = RouteTemplateHelper.GetRouteTemplate(methodInfo.DeclaringType!, methodInfo); + + var routeParameters = new HashSet( + Regex.Matches(path, @"{(\w+)}") + .Cast() + .Where(x => x.Success && x.Groups.Count > 1 && x.Groups[1].Success) + .Select(x => x.Groups[1].Value) + ); + + var queryParams = GetMethodParameters(methodInfo) + .Where(x => !IsFromBody(x) && !routeParameters.Contains(x.Name)) + .Select(x => $"{x.Name}={{{x.Name}}}") + .ToArray(); + + var query = queryParams.Any() + ? "?" + string.Join("&", queryParams) + : string.Empty; + + return $"{path}{query}"; + } + + public TypeScriptType GetMethodResultType(IMethodInfo methodInfo, Func buildAndImportType) + { + return IsUrlMethod(methodInfo) + ? (TypeScriptType)new TypeScriptTypeReference("string") + : new TypeScriptPromiseOfType(buildAndImportType(ResolveReturnType(methodInfo.ReturnType))); + } + + public TypeScriptExpression? GetMethodBodyExpression(IMethodInfo methodInfo) + { + var parameter = GetMethodParameters(methodInfo).SingleOrDefault(IsFromBody); + return parameter == null + ? null + : new TypeScriptVariableReference(parameter.Name); + } + + private static bool IsFromBody(IParameterInfo parameterInfo) + { + return parameterInfo + .GetAttributes(inherit : false) + .Any(a => a.HasName(KnownTypeNames.Attributes.FromBody)); + } + + private static ITypeInfo ResolveReturnType(ITypeInfo typeInfo) + { + if (typeInfo.IsGenericType) + { + var genericTypeDefinition = typeInfo.GetGenericTypeDefinition(); + if (genericTypeDefinition.Equals(TypeInfo.From(typeof(Task<>))) || + genericTypeDefinition.Name == KnownTypeNames.ActionResultOfT || + genericTypeDefinition.Name == KnownTypeNames.ActionResult) + return ResolveReturnType(typeInfo.GetGenericArguments()[0]); + } + + if (typeInfo.Equals(TypeInfo.From()) || + typeInfo.Name == KnownTypeNames.ActionResult || + typeInfo.Name == KnownTypeNames.IActionResult) + return TypeInfo.From(typeof(void)); + + return typeInfo; + } + } +} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/IApiCustomization.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/IApiCustomization.cs new file mode 100644 index 0000000..541a3e0 --- /dev/null +++ b/TypeScript.ContractGenerator/TypeBuilders/ApiController/IApiCustomization.cs @@ -0,0 +1,27 @@ +using System; + +using SkbKontur.TypeScript.ContractGenerator.Abstractions; +using SkbKontur.TypeScript.ContractGenerator.CodeDom; + +namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController +{ + public interface IApiCustomization + { + public TypeLocation GetApiBase(ITypeInfo type); + public TypeLocation GetUrlTag(ITypeInfo type); + + string GetApiClassName(ITypeInfo type); + string GetApiInterfaceName(ITypeInfo type); + string GetMethodName(IMethodInfo methodInfo); + + IMethodInfo[] GetApiMethods(ITypeInfo type); + IParameterInfo[] GetMethodParameters(IMethodInfo methodInfo); + + bool IsUrlMethod(IMethodInfo methodInfo); + bool IsAsyncMethod(IMethodInfo methodInfo); + string GetMethodVerb(IMethodInfo methodInfo); + string GetMethodRoute(IMethodInfo methodInfo); + TypeScriptType GetMethodResultType(IMethodInfo methodInfo, Func buildAndImportType); + TypeScriptExpression? GetMethodBodyExpression(IMethodInfo methodInfo); + } +} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/KnownTypeNames.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/KnownTypeNames.cs index b821bb3..fb2a994 100644 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/KnownTypeNames.cs +++ b/TypeScript.ContractGenerator/TypeBuilders/ApiController/KnownTypeNames.cs @@ -6,6 +6,7 @@ public static class KnownTypeNames { public const string ActionResultOfT = "ActionResult`1"; public const string ActionResult = "ActionResult"; + public const string IActionResult = "IActionResult"; public static readonly HashSet HttpAttributeNames = new HashSet { diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplate.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplate.cs deleted file mode 100644 index 0d19f0e..0000000 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplate.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController -{ - public class RouteTemplate : IEquatable - { - public RouteTemplate(string value) - { - this.value = value; - ParameterNames = GetParameterNames(value); - ValueWithoutConstraints = Regex.Replace(value, @"{(\w+):\w+}", "{$1}"); - } - - public HashSet ParameterNames { get; } - - public string ValueWithoutConstraints { get; } - - public override string ToString() => value; - - private static HashSet GetParameterNames(string routeTemplate) - { - return new HashSet( - Regex - .Matches(routeTemplate, @"{(\w+):\w+}") - .Cast() - .Where(x => x.Success) - .Where(x => x.Groups.Count > 1) - .Where(x => x.Groups[1].Success) - .Select(x => x.Groups[1].Value)); - } - - public bool Equals(RouteTemplate? other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return value == other.value; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((RouteTemplate)obj); - } - - public override int GetHashCode() - { - return value.GetHashCode(); - } - - private readonly string value; - } -} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplateHelper.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplateHelper.cs index 49fe4bf..6e6ac6a 100644 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplateHelper.cs +++ b/TypeScript.ContractGenerator/TypeBuilders/ApiController/RouteTemplateHelper.cs @@ -8,7 +8,18 @@ namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController { public static class RouteTemplateHelper { - public static RouteTemplate? FindFullRouteTemplate(ITypeInfo controller, IMethodInfo action) + public static string GetRouteTemplate(ITypeInfo controller, IMethodInfo action) + { + var rawTemplate = FindFullRouteTemplate(controller, action); + + var valueWithoutConstraints = Regex.Replace(rawTemplate, @"{(\w+):\w+}", "{$1}"); + if (valueWithoutConstraints.StartsWith("/")) + return valueWithoutConstraints; + + return "/" + valueWithoutConstraints; + } + + private static string FindFullRouteTemplate(ITypeInfo controller, IMethodInfo action) { var routePrefix = (controller .GetAttributes(inherit : true) @@ -26,15 +37,15 @@ public static class RouteTemplateHelper .Replace("[action]", action.Name); if (routeTemplate.StartsWith("~/")) - return new RouteTemplate(Regex.Replace(routeTemplate, "^~/?", "")); + return Regex.Replace(routeTemplate, "^~/?", ""); if (string.IsNullOrEmpty(routePrefix)) - return new RouteTemplate(routeTemplate); + return routeTemplate; if (string.IsNullOrEmpty(routeTemplate)) - return new RouteTemplate(routePrefix); + return routePrefix; - return new RouteTemplate($"{routePrefix}/{routeTemplate}"); + return $"{routePrefix}/{routeTemplate}"; } } } \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/ApiController/UrlOnlyAttribute.cs b/TypeScript.ContractGenerator/TypeBuilders/ApiController/UrlOnlyAttribute.cs deleted file mode 100644 index b3baa60..0000000 --- a/TypeScript.ContractGenerator/TypeBuilders/ApiController/UrlOnlyAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders.ApiController -{ - public class UrlOnlyAttribute : Attribute - { - } -} \ No newline at end of file