NeVeSpl / NTypewriter

File/code generator using Scriban text templates populated with C# code metadata from Roslyn API.
https://nevespl.github.io/NTypewriter/
MIT License
126 stars 24 forks source link

[Question] Get enum names and values from IType #59

Closed satfx closed 2 years ago

satfx commented 2 years ago

Hello again!

I'm trying to create template for my enums, and stuck with discovering enum names and values from IType. Why from IType? It is because I'm using data.Classes as entry point and discover all referenced types (via built-in function TypeFunctions.AllReferencedTypes) from all parameter and return types of all public actions of my controllers.

Template:

{{- for type in data.Classes | Custom.ExportTypes | Custom.ThatAreEnums
    capture output
}}
export enum {{ type.Name }} {
    {{- for item in type | Custom.EnumValues }}
    {{ item.Name }} = {{ item.Value }},
    {{- end }}
}
{{- end
    Save output ("types\\" + type.Name + ".generated.ts")
end
}}

The custom EnumValues function:

public static IEnumerable<IEnumValue> EnumValues( this IType source )
{
    if (!source.IsEnum)
        return Enumerable.Empty<IEnumValue>();

    // TODO: ???
}

NTypewriter v0.3.4

gregveres commented 2 years ago

Here is my _Enums.nt file that exports enums to typescript. The barrel file stuff is about collecting all of them into single export file. The rest is pretty straight forward for extracting an enum that looks like:

export enum ActivityState {
  PrePublished = 0,
  Published = 1,
  Archived = 2
}

here is the Ntypewriter file. Also note that I use an attribute called [ExportToTypescript] as the way to signify which classes/enums to generate into typescript.

{{ $barrelFile = "" }}
{{- for enum in data.Enums | Symbols.ThatHaveAttribute "ExportToTypescript" | Array.Sort "Name" -}}
{{- capture output -}}
export enum {{enum.Name}} {
  {{- for enumValue in enum.Values}}
  {{ enumValue.Name}} = {{ enumValue.Value}}{{-if !for.last}},{{end}}
  {{-end}}
}
{{- end}}
{{- Save output ("..\\..\\..\\SkyCourt.UI\\skycourt\\libs\\api\\enums\\src\\lib\\" | String.Append enum.BareName | String.Append ".ts")}}
{{- capture barrelOutput -}}
export { {{enum.Name}} } from './lib/{{enum.Name}}';
{{- end -}}
{{ $barrelFile = $barrelFile | String.Append barrelOutput | String.Append "\n" }}
{{- end}}
{{- Save $barrelFile ("..\\..\\..\\SkyCourt.UI\\skycourt\\libs\\api\\enums\\src\\index.ts")}}

I read your question a bit closer. I think what you need is that first part for enum in data.Enums instead of what you have: for type in data.Classes

satfx commented 2 years ago

Thanks, Greg, for a really lightning speed reply! I prefer not to count on any additional attributes (programmers have very bad memory and are really, REALLY lazy persons), nor publishing all my enums from the whole project, but only those that are really used in visible controllers and actions.

Btw, I saw example templates in the test repository "https://github.com/NeVeSpl/TestMe/tree/master/TestMe.Presentation.React/ClientApp/src/autoapi2"

NeVeSpl commented 2 years ago

It is possible to achieve what you want on 0.3.4, but that would require a lot of ugly code in the custom function. In 0.3.5 it will be much simpler:

{{- for class in data.Classes 
       for enum in class| Type.AllReferencedTypes 
         if !enum.IsEnum
            break
         end
        capture output
}}

export enum {{ enum.Name }} 
{
{{- for item in enum.Values }}
    {{ item.Name }} = {{ item.Value }},   
{{- end }}
}
{{-     end

   Save output ("enums\\" + enum.FullName + ".tsx")
    end
   end 
}}

Your template from the fist post also should work on 0.3.5 without the need for Custom.EnumValues.

satfx commented 2 years ago

(facepalm) I'm really sorry for bothering you, guys, but it is all fine now! I don't know why initially I received an error when trying to use "Values" in the expression "{{- for item in type.Values }}" in my template from the first post. MB I accidentally used "EnumValues" instead of "Values". Anyway thanks for your help and a tool that is a pleasure to work with!

Source code for the ExportTypes, mb it will help somebody:

private const string ControllerSuffix = "Controller";

public static IEnumerable<IType> ExportTypes( this IEnumerable<IClass> source )
{
    var controllers = source
        .ThatAreApiControllers()
        .WhereControllerInDefaultArea();
    var methods = controllers
        .SelectMany(v => v.Methods)
        .ThatAreActions();
    return methods
        .SelectMany(v => v.Parameters().Select(x => x.Type).Append(v.ReturnType()))
        .SelectMany(v => v.AllReferencedTypes(SearchIn.BaseClass | SearchIn.Properties).Append(v))
        .Distinct(new TypeComparer());
}

public static IEnumerable<IClass> ThatAreControllers( this IEnumerable<IClass> source )
    => source
        .Where(v => !v.IsAbstract)
        .Where(v => IsSubclassOf(v, "ControllerBase") || v.Attributes.Any(x => x.Name == "Controller"))
        .Where(v => v.Attributes.All(x => x.Name != "NonController"));

public static IEnumerable<IClass> ThatAreApiControllers( this IEnumerable<IClass> source )
    => source
        .ThatAreControllers()
        .Where(v => v.Attributes.Any(x => x.Name == "ApiController"));

public static IEnumerable<IClass> ThatAreNotApiControllers( this IEnumerable<IClass> source )
    => source
        .ThatAreControllers()
        .Where(v => v.Attributes.All(x => x.Name != "ApiController"));

private static bool IsSubclassOf( IType source, string baseTypeName )
{
    while (source != null)
    {
        if (source.Name == baseTypeName)
            return true;

        source = source.BaseType;
    }
    return false;
}

public static string AreaName( this IType source )
    => source.Attributes
        .Where(v => v.Name == "Area")
        .SelectMany(v => v.Arguments)
        .Where(v => v.Name == "areaName")
        .Select(v => (string)v.Value)
        .FirstOrDefault();

public static string ControllerName( this IType source )
    => source.Name.EndsWith(ControllerSuffix)
        ? source.Name.Substring(0, source.Name.Length - ControllerSuffix.Length)
        : source.Name;

public static IEnumerable<IClass> WhereControllerInArea( this IEnumerable<IClass> source, string areaName )
    => source.Where(v => v.AreaName() == areaName);

public static IEnumerable<IClass> WhereControllerNotInArea( this IEnumerable<IClass> source, string areaName )
    => source.Where(v => v.AreaName() != areaName);

public static IEnumerable<IClass> WhereControllerInDefaultArea( this IEnumerable<IClass> source )
    => WhereControllerInArea(source, null);

public static IEnumerable<IMethod> ThatAreActions( this IEnumerable<IMethod> source )
    => source
        .Where(v => v.IsPublic && !v.IsStatic)
        .Where(v => v.Attributes.All(x => x.Name != "NonAction"));

public static string ActionName( this IMethod source )
    => ActionNameFromAttribute(source) ?? ActionNameFromMethod(source);

private static string ActionNameFromAttribute( IMethod source )
    => source.Attributes
        .Where(v => v.Name == "ActionName")
        .SelectMany(v => v.Arguments)
        .Where(v => v.Name == "name")
        .Select(v => (string)v.Value)
        .FirstOrDefault();

private static string ActionNameFromMethod( IMethod source )
    => source.Name.EndsWith("Async")
        ? source.Name.Substring(0, source.Name.Length - 5)
        : source.Name;

internal class TypeComparer : IEqualityComparer<IType>
{
    public bool Equals( IType x, IType y )
        => x.FullName.Equals(y.FullName);

    public int GetHashCode( IType obj )
        => obj.FullName.GetHashCode();
}