frhagn / Typewriter

Automatic TypeScript template generation from C# source files
http://frhagn.github.io/Typewriter
Apache License 2.0
537 stars 132 forks source link

Angular service with multiple response types (ASP.NET CORE) #336

Open luizfbicalho opened 3 years ago

luizfbicalho commented 3 years ago

Did Anyone make a tst to generate a full angular service that handle multiple response types? It would be perfect with error treatment, retry, feature toggle. If someone started on this, I would like to help the implementation. this is my initial implementation

${

    // Enable extension methods by adding using Typewriter.Extensions.*
    using Typewriter.Extensions.WebApi;
    using Typewriter.Extensions.Types;

    // Uncomment the constructor to change template settings.
    Template(Settings settings)
    {
    //    settings.IncludeProject("Project.Name");
            settings.OutputFilenameFactory = file=> ModelName(file.Classes.First()).ToLower()+".service.ts";
    }

    // Custom extension methods can be used in the template by adding a $ prefix e.g. $LoudName
    string LoudName(Property property)
    {
        return property.Name.ToUpperInvariant();
    }

    string ServiceName(Class clas)
    {
        return clas.Name.Replace("Controller","Service");
    }
    string ModelName(Class clas)
    {
        return clas.Name.Replace("Controller","");
    }

    string MethodType(Method method)
    {
        if(method.Type=="DataSourceResult")
        {
            return "GridDataResult";
        }

        if (method.Parent is Class baseClasse)
        {
            if ( baseClasse.Name == "CrudController" )
            {
                return method.Name == "Get" || method.name == "PostAll" || method.name == "PutAll" ?
                        baseClasse.TypeArguments[0].Name + "[]" : baseClasse.TypeArguments[0].Name;
            }
        }

        var att = method.Attributes.Where(x=>x.FullName=="Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute" && x.Arguments.Last().Value is int value && value < 299 ).OrderBy(x=> x.Arguments.Last().Value is int value).FirstOrDefault();

        if(att !=null)
        {
            if(!att.Value.Contains("typeof("))
            {
            return "void";
            }
            var type = att.Value.Replace("typeof(","").Replace(")","").Split('.').Last().Split(',').First(); 
            type = type.Replace("IEnumerable","List");
            if(type=="DataSourceResult")
            {
                return "GridDataResult";
            }
            if(att.Value.Contains("IEnumerable") || att.Value.Contains("IList"))
            {
                type = type + "[]";
            }
            return  type.Replace(">","" ) ;
        }

        if(method.Type.Name.Contains("[]"))
        {
            var retorno = method.Type.Name;
            return retorno;
        }

        return method.Type;
    }
    string PostPutData(Method method)
    {
        var parameter = GetPostPutParameter(method);
        if(parameter!=null)
        {
            return "this.stringfy"+method.Name+"( "+FilteredRequestData( method)+"),";
        }
        if( method.HttpMethod() == "post" || method.HttpMethod() == "put")
        {
            return FilteredRequestData( method)+",";
        }
        return string.Empty;
    }

    Parameter GetPostPutParameter(Method method)
    {
        if( method.HttpMethod() == "post" || method.HttpMethod() == "put")
        {
            var parameter = FilteredRequestParameter(method);
            if(parameter !=null)
            {
                var propertyType = ParameterType(parameter);
                if(propertyType!= "DataSourceRequestState")
                {
                    return parameter;
                }
            }
        }
        return null;

    }

    bool IsSubForm(Property prop)
    {
        return prop.Attributes.Any(x=>x.FullName.Contains("SubForm"));
    }

    string internalPostPutMethod(Type type,string methodName,List<Property> addProperties)
    {
        var retorno = string.Empty;
        foreach(var prop in  AssociationProperties(type).Where(x=>!x.Type.IsEnum))
        {
            retorno += 
            "\n\t\tif (parameter."+prop.Name+") {";
            if(!IsSubForm(prop))
            {

                retorno += "\n\t\t   parameter."+prop.Name+" = EntidadeId.CreateFrom(parameter."+prop.Name+");";

            }
            else
            {
                retorno += "\n\t\t   parameter."+prop.Name+" = this.stringfy"+methodName+prop.Name+"(parameter."+prop.Name+");";
                addProperties.Add(prop);
            }
            retorno += "\n\t\t}"+
            "\n\t\telse {"+
            "\n\t\t    delete parameter."+prop.Name+";"+
            "\n\t\t}";
        }
        foreach(var prop in  MultipleAssociationProperties(type).Where(x=>!x.Type.IsEnum))
        {
            retorno += 
            "\n\t\tparameter."+prop.Name+" = List.getArrayList(parameter."+prop.Name+", x => {";
            if(!IsSubForm(prop))
            {

                retorno += "\n\t\t   return EntidadeId.CreateFrom(x);";

            }
            else
            {
                retorno += "\n\t\t   return this.stringfy"+methodName+prop.Name+"(x);";
                addProperties.Add(prop);
            }

            retorno +="\n\t\t});";
        }
        return retorno;
    }

    string PostPutMethod(Method method)
    {
        List<Property> addProperties = new List<Property>();
        var retorno = string.Empty;
        var parameter = GetPostPutParameter(method);

        if(parameter!=null)
        {
            var propertyType = ParameterType(parameter);
            if(propertyType!= "DataSourceRequestState")
            {
                retorno = "\n\tprivate stringfy"+method.Name+"(p:"+propertyType+"):string {"+
                "\n\t\tlet parameter = EntidadeId.Clone(p);";
                retorno += internalPostPutMethod(parameter.Type,method.Name,addProperties);
                retorno += "\n\t\treturn JSON.stringify(parameter);"+
                "\n\t}\n";
            }
        }
        while(addProperties.Any())
        {
            foreach(var prop in addProperties.ToList())
            {
                var type = prop.Type.Unwrap();
                 retorno += "\n\tprivate stringfy"+method.Name+prop.Name+"(p:"+type.Name+"):any {"+
                    "\n\t\tlet parameter = EntidadeId.Clone(p);";
                    retorno += internalPostPutMethod(type,method.Name,addProperties);
                    retorno += "\n\t\treturn parameter;"+
                    "\n\t}\n";
                 addProperties.Remove(prop);
            }
        }
        return retorno;

    }
    Parameter FilteredRequestParameter(Method method)
    {
        var url = method.Url();
        var dataParameters = FilteredParameters(method).Where(p => url.Contains($"${{{p.Name}}}") == false).ToList();

        if (dataParameters.Count == 1)
        {
            return dataParameters.First();
        }
        return null;
    }
    string FilteredRequestData(Method method)
    {
        var url = method.Url();
        var dataParameters = FilteredParameters(method).Where(p => url.Contains($"${{{p.Name}}}") == false).ToList();

        if (dataParameters.Count == 1)
        {
            return dataParameters.First().Name;
        }

        if (dataParameters.Count > 1)
        {
            return $"{{ {string.Join(", ", dataParameters.Select(p => $"{p.Name}: {p.Name}"))} }}";
        }

        return "null";
    }
    string ModelNameLower(Class cla)
    {
        return ModelName(cla).ToLower();
    }
    string Map(Method method)
    {
        var name = method.Type.Name;
        var isList = false;

        var isCrudController = false;

        if(method.Type=="DataSourceResult")
        {
            return "return  getGridDataResult(res.body,(request.group && request.group.length));";
        }

        if (method.Parent is Class baseClasse)
        {
            if ( baseClasse.Name == "CrudController" )
            {
                isCrudController = true;

                name = baseClasse.TypeArguments[0].Name;

                isList =  method.Name == "Get" || method.name == "PostAll" || method.name == "PutAll";
            }
        }

        var att = method.Attributes.Where(x=>x.FullName=="Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute" && x.Arguments.Last().Value is int value && value < 299 ).OrderBy(x=> x.Arguments.Last().Value is int value).FirstOrDefault();

        if(att !=null && isCrudController == false)
        {

            var type = att.Value.Replace("typeof(","").Replace(")","").Split('.').Last().Split(',').First(); 
            if(!att.Value.Contains("typeof("))
            {
                type = "void";
            }

            if(att.Value.Contains("IEnumerable<"))
            {
                isList = true;
                type = type.Replace("IEnumerable<","").Replace(">","");
            }
            if(!isList)
            {
                if(type!="void")
                {
                    return "return new " + type + "( res.body );";
                }
                else
                {
                    return "return Observable.create((observer) => {});";

                }
            }
        }
        if(method.Type.name.Contains("[]") || isList )
        {
            name = name.Replace("[]","");

            return "return ((<any>res.body).$values).map(item => { "+
                   "return new "+name+"(item); " +
                   "});";
        }

        if(method.Type.name == "any")
        {
            return "return res.body;";
        }
        //return string.Join(" ", method.Attributes.Select(x=>x.FullName));
        return     "return new " + name + "(res.body );";
    }   
    string ImportReference(Typewriter.CodeModel.Class clas)
    {
        var retorno = new List<string>();
        foreach(var method in clas.Methods.Union(clas.BaseClass.Methods))
        {

            var prop = MethodType(method);
            var propName = prop.Replace("[]","");
            if(propName.IndexOf("<")>=0)
            {
                var inicio = propName.IndexOf("<")+1;
                propName = propName.Substring(inicio,propName.Length-1-inicio);
            }
            if( propName != "string" 
                && propName != "number" 
                && propName != "Date"
                && propName != "boolean"
                && propName != "any"
                && propName != "void"
                && propName != "List"
                && propName != ModelName(clas))
            { 
                if(propName == "GridDataResult")
                {
                    retorno.Add( "import { DataSourceRequestState } from '@progress/kendo-data-query';" );    
                    retorno.Add( "import { GridDataResult } from '@progress/kendo-angular-grid';" );    
                    retorno.Add( "import { getGridDataResult } from 'local/shared/models/griddataresult.model';");
                }
                else
                {
                    retorno.Add( "import { " + propName + " } from 'local/models/" +propName.ToLower()+".model';" ); 
                }
            }
        }

        return string.Join(Environment.NewLine, retorno.Distinct());
    }
    Parameter[] FilteredParameters(Method method)
    {
        return method.Parameters.Where(x=>x.Type.Name.ToLower() != "cancellationtoken").ToArray();
    }

    string ParameterType(Parameter parameter)
    {
        if(parameter.Type=="DataSourceRequest")
        {
            return "DataSourceRequestState";
        }
        return parameter.Type;
    }

    IEnumerable<Property> ModelProperties(Type clas)
    {
        var retorno = AllModelProperties(clas).Where(x=>!KeyProperty(x)).ToList();
        return retorno;
    }
    bool KeyProperty(Property property)
    {
        return property.Attributes.Any(x=>x.Name == "Key" || property.Name == "Id");
    }

    IEnumerable<Property> AllModelProperties(Type clas)
    {
        List<Property> retorno = new List<Property>();
        if(clas!=null)
        {
            var type = clas;

            retorno.AddRange(GetAllPropertiesFiltered(type,type));
            var c = type.BaseClass;
            while(c!=null)
            {
                retorno.AddRange(GetAllPropertiesFiltered(type,c));
                c = c.BaseClass;
            }
        }
        return retorno;
    }
    IEnumerable<Property> GetAllPropertiesFiltered(Type clas,Type baseclass)
    {
        var retorno = baseclass.Properties.ToList();
        retorno = retorno.Where(x=>!x.Attributes.Any(q=>q.FullName.Contains("ScaffoldColumn") && q.Value.ToLower()=="false")).ToList();
        if(clas.Attributes.Any(w=>w.FullName.Contains("DataContract")))
        {
            retorno = retorno.Where(x=>x.Attributes.Any(q=>q.FullName.Contains("DataMember"))).ToList();
        }
        return retorno;//.Where(x=>!x.Type.IsEnum);
    }
     IEnumerable<Property> AssociationProperties(Type clas)
     {
        return ModelProperties(clas).Where(x=> {
        var propName = x.Type.Name;
                return !( propName == "string" 
                 || propName == "number" 
                 || propName == "Date"
                 || propName == "boolean"
                 || propName == "any"
                 || propName == "void") && !propName.Contains("[]");

        });
     }
     IEnumerable<Property> MultipleAssociationProperties(Type clas)
     {
        return ModelProperties(clas).Where(x=> {
        var propName = x.Type.Name;
                return !( propName == "string" 
                 || propName == "number" 
                 || propName == "Date"
                 || propName == "boolean"
                 || propName == "any"
                 || propName == "void") && propName.Contains("[]");

        });
     }

     Type SubFormType(Class clas)
     {
        return ModelPropertiesEnums(clas).Where(x=>IsSubForm(x) ).Select(x=>x.Type).FirstOrDefault();
     }
     IEnumerable<Property> Enums(Class clas)
     {

        var retorno = new List<Property>();
        retorno.AddRange( ModelPropertiesEnums(clas).Where(x=>x.Type.IsEnum));
        var sub = SubFormType(clas);
        if(sub !=null)
        {
            if(sub.name.Contains("[]"))
            {
                sub = sub.Unwrap();
            }
            retorno.AddRange( ModelProperties(sub).Where(x=>x.Type.IsEnum));
        }

        return retorno;
     }

     string NameAutocomplete(Property prop)
     {
        return prop.name + "Autocomplete";
     }
     IEnumerable<EnumValue> EnumValues(Property clas)
     {
        return new List<EnumValue>();
     }
     string PropertyDescription(EnumValue prop)
     {
        var att = prop.Attributes.Where(x=>x.FullName.Contains("Display")).FirstOrDefault();
        if(att!=null)
        {
            return att.Value.Split('=').Last().Trim().Replace("\"","");
        }
        return prop.Name;
     }

     IEnumerable<Property> ModelPropertiesEnums(Type clas)
    {
        List<Property> retorno = new List<Property>();
        if(clas.BaseClass.IsGeneric)
        {
            var type = clas.BaseClass.TypeArguments.FirstOrDefault();
            if(type !=null)
            {
                retorno.AddRange(GetAllPropertiesFiltered(type,type));
                var c = type.BaseClass;
                while(c!=null)
                {
                    retorno.AddRange(GetAllPropertiesFiltered(type,c));
                    c = c.BaseClass;
                }
                return retorno;
           }
        }
        foreach(var method in clas.Methods)
        {
            var type = method.Type;
            while(type.name != type.Unwrap().name)
            {
                type = type.Unwrap();
            }
            if(method.name.ToLower().Contains("create")
                && type.name.ToLower() == clas.Name.Replace("Controller","").ToLower())
            {
                retorno.AddRange(GetAllPropertiesFiltered(type,type));
                var c = type.BaseClass;
                while(c!=null)
                {
                    retorno.AddRange(GetAllPropertiesFiltered(type,c));
                    c = c.BaseClass;
                }
            }
        }
        return retorno;
    }

    IEnumerable<Method> BaseClassMethods(Class clas)
    {
        return clas.BaseClass.Methods;
    }

    string URLClassBase(Method method)
    {
        var url = UrlExtensions.Url(method);

        if (method.Parent is Class baseClasse)
        {
            if ( baseClasse.Name == "CrudController" )
            {
                 // Caso não tenha rota
                 if ( url.Contains("api") )
                 {
                    url = String.Empty;
                 }

                return $"api/{(baseClasse.Parent as Class).Name.Replace("Controller","")}/{url}";
            }
        }

        return url;
    }

}

$Classes(*Controller*)[
import {Injectable,Inject} from '@angular/core';
import {HttpError} from 'local/shared/models/httperror';
import {ModelState} from 'local/shared/models/modelstate';
import {List } from 'local/shared/models/list.model';
import {HttpClient, HttpResponse, HttpErrorResponse} from '@angular/common/http';
import {Observable, of }  from 'rxjs';
import {EntidadeId} from 'local/models/entidadeid.model'
import {$ModelName} from 'local/models/$ModelNameLower.model';

import { map, catchError } from 'rxjs/operators';

import { throwError } from 'rxjs';

$ImportReference

@Injectable()
export class $ServiceName {
    constructor(private $http: HttpClient,
    @Inject('ApiEndpoint') private apiEndpoint: string) { } 
    $Methods[

    public $name = ($FilteredParameters[$name: $ParameterType][, ]) : Observable<$MethodType> => {
       return this.$http.$HttpMethod<$MethodType>(this.apiEndpoint + `$Url`, $PostPutData {observe: "response" })
       .pipe(
            map((res: HttpResponse<$MethodType>) => { $Map}),
            catchError((error: HttpErrorResponse) => throwError(error))
        );
    }
    $PostPutMethod
    ]
    $BaseClassMethods[

    public $name = ($FilteredParameters[$name: $ParameterType][, ]) : Observable<$MethodType> => {
       return this.$http.$HttpMethod<$MethodType>(this.apiEndpoint + `$URLClassBase`, $PostPutData {observe: "response" })
       .pipe(
            map((res: HttpResponse<$MethodType>) => { $Map}),
            catchError((error: HttpErrorResponse) => throwError(error))
        );
    }
    $PostPutMethod
    ]
    $Enums[
        public $NameAutocomplete = (): Observable<{ text: string; value: number; }[]> => {
            // colocar os itens dos enums enquanto não estiver funcionando a geracao.
            var retorno = [
            $EnumValues[{ text: "$PropertyDescription", value: $Value }][,]
        ];
        return of(retorno);

    }
    ]

}]
AdaskoTheBeAsT commented 3 years ago

Hi,

yes you can take a look at https://github.com/AdaskoTheBeAsT/NetCoreTypewriterRecipes/blob/master/src/AngularWebApiSample/ClientApp/src/api/services/_AutogeneratedServices.tst https://github.com/AdaskoTheBeAsT/NetCoreTypewriterRecipes/blob/master/src/AngularWebApiSample/ClientApp/src/api/services/_AutogeneratedServices.tst

model classes are separated from services - full inheritance of model classes is implemented with different type collections I use ProducesResponseType attribute extensively.

Also for HttpGet method I use trick to compose class from parameters passed in url