Skip to content
Pavel Novikov edited this page Oct 7, 2019 · 3 revisions

Reinforced.Typings customization technics

Besides out-of-the-box features, Reinforced.Typings supplies flexible approaches to customize TypeScript output by providing varios extension points. Hereby I will briefly tell about how to use them to achieve your goals.

How RT works?

Reinforced.Typings work through AST. Well, it does not actually contain all the TypeScript AST - it is obviously overkill, but just main elements - classes, fields, functions, constructors etc. AST nodes are the classes that inherit RtNode base class - it is not interesting since contains only members necessary for visiting. Full list of AST nodes can be observed here, in the code. So it constructs AST first, then generates resulting file by using visitors. Visitors have given file stream and they serialize AST into it. RT manages file streams for visitors by itself (to count in your files settings), so you do not have to care about file streams when working with this functionality.

Actually things described above are more or less well-known industy approach.

Well and how AST is being generated then? Answer is pretty simple - there are code generators for each class member. And good news are that you can flexibly inherit and override to achieve various goals.

Code generators

The base of all the code generators is ITsCodeGenerator<TElement> interface where TElement is either FieldInfo, PropertyInfo, MethodInfo or Type. But actually I bet that you will not inherit and implement this interface directly because there are too much subtleties and tricks for each case. Instead of that - let's take a look at existing generators:

Lifetime: code generators are being created once for RT run. So when you inherit them - you can store your information in its instance variables. And also keep in mind that code generators are being invoked during build time, when your application actually is not running. So pay attention to use of static things from your project. Also it means that you cannot use e.g. IoC or MVC abilities.

Visitors

Visitors inherit TextExportingVisitor that inerits VisitorBase that implements IRtVisitor). Currently RT has 2 types of visitors implemented:

  • TypeSript Visitor: Produces general TypeScript code from AST nodes. Output suits .ts file;
  • Typings Visitor: Inherits TypeScript visitor and produces declarations (.d.ts) from same AST nodes.

You can inherit one of them and override the Visit* methods in order to deeply customize output.

How do I use code generators?

Basic flow is:

  1. Inherit existing code generator that fits your abilities, customize it
  2. Point RT to use this code generator

Assume that we have implemented following code generators:

public class AngularClassCodeGenerator : ClassCodeGenerator 
{
    // fold
}
public class AngularMethodCodeGenerator : MethodCodeGenerator
{
    // fold
}

public class CustomEnumGenerator : EnumGenerator
{
    // fold
}

public class MyPropertyGenerator : PropertyCodeGenerator
{
    // fold
}

You can tell RT to use them in 2 ways basically.

With attributes configuration

In case of using RT attributes you can utilize CodeGeneratorType that presents in almost every attribute:

[TsClass(CodeGeneratorType = typeof(AngularClassCodeGenerator))]
public class CodeGeneratedClass
{
    [TsProperty(CodeGeneratorType = typeof(MyPropertyGenerator))]
    public string MyProperty {get;set;}
}

In this case be careful to point suitable code generator type. The checks are performed in run-time (well, for your project it is compile-time of course, but run-time for RT) - so you will get an exception in case if supplied code generator does not fit. Skipping long type inference explanations, let's say that you have to inherit and use:

  • ClassCodeGenerator/InterfaceCodeGenerator for classes and interfaces
  • PropertyCodeGenerator for properties
  • MethodCodeGenerator for methods
  • EnumGenerator for enums
  • FieldCodeGenerator for fields

You also can inherit RT's attributes to do not always specify code generator explicitly:

using Reinforced.Typings.Attributes;

public class TsAngularWebControllerAttribute : TsClassAttribute
{
    public TsAngularWebControllerAttribute()
    {
        CodeGeneratorType = typeof(AngularClassCodeGenerator);
    }
}

[TsAngularWebController]
public class MyController
{
    // fold
}

With fluent configuration

Fluent calls .ExportAs* has also extension method .WithCodeGenerator<>(). As well as .With* fluent calls for class members also have this extension. There are static type checks, so here you are protected by compiler from making a mistake:

public static void ConfigureTypings(ConfigurationBuilder builder)
{
    builder.ExportAsClass<MyController>()
        .WithPublicMethods(x=>x.WithCodeGenerator<AngularMethodCodeGenerator>())
        .WithCodeGenerator<AngularClassCodeGenerator>();

    builder.ExportAsEnum<MyEnum>().WithCodeGenerator<CustomEnumGenerator>();
}

What we have within generator?

Let's look at class code generator if you would like to inherit it:

using Reinforced.Typings.Generators;
using Reinforced.Typings.Ast;

public class FizzClassCodeGenerator : ClassCodeGenerator
{
    public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
    {
        // you are here
        // Rt has already created RtClass for you - so you can change it
        // and then return. Just like that
        result.Name = new RtSimpleTypeName("Hi Generator!");
        return result;

        // You can just return null to suppress exporting this class into resulting file
        // It also works with all other generators. 
        // Simply - if you don't want to see some member generated - just return null
        return null;        
    }
}

Generators commonly have one method to override - the GenerateNode method. There you receive your Type or PropertyInfo or MethodInfo. Also you receive instance of TypeResolver that you can utilize to convert your CLR types into TypeScript ones according to current RT configuration. RT uses RtTypeName AST node to identify various types - feel free to instantiate it directly (it has variants for Arrays, Objects, Tuples, Delegates), but use of TypeResolver is more elegant.

Well, another useful thing that you have within code generator is export context. It can be accessed like that:

using Reinforced.Typings.Generators;
using Reinforced.Typings.Ast;

public class FizzClassCodeGenerator : ClassCodeGenerator
{
    public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
    {
        // obtain current namespace from context
        var ns = this.Context.Location.CurrentNamespace;
        return null;        
    }
}

Context is of type ExportContext. Let's note some useful fields that it has:

  • Location: can tell you where you currently are. You can obtain (probably, unfinished) AST of current class, current interface, current namespace and alter it;
  • SourceAssemblies: array that gives you access to assemblies that exported types are being taken from;
  • Documentation: reference to documentation manager that helps you to retrieve documentation entries for various CLR entities;
  • Warnings: list that you can add your custom warnings - they will go to VisualStudio's corresponding panel;
  • Global: reference to global parameters specified by [TsGlobal] attributes or corresponding fluent calls;
  • Generators: reference to generators manager. In case if you need some other generator to invoke - you can obtain its insance from generators manager. It also respects generators configuration, so use it instead of manually instantiating generators - it will help you to maintain flexibility further;
  • Access to blueprints (see below)

Alter the output

The most easiest way to tweak and customize your TypeScript output is to obtain the AST that RT has automatically generated and change it. Look:

using Reinforced.Typings.Generators;
using Reinforced.Typings.Ast;

public class FizzClassCodeGenerator : ClassCodeGenerator
{
    public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
    {
        // Obtain AST node generated by RT
        var r = base.GenerateNode(element, result, resolver);
        // in case if RT decided to omit this node...
        if (r==null) return null;

        // what we can do here? hmm... let's add new field
        r.Members.Add(new RtField()
            {
                Identifier = new RtIdentifier("DynamicField"),
                Type = resolver.ResolveTypeName(typeof(Func<int,string>))
            });
        // this will add new field named "DynamicField" into class that is being generated
        // it will have type pf (arg: number) => string
        // (I also show that TypeResolver handles delegates successfully)

        // let's also add decorator to our class
        // so it will be like 
        // @with_dynamic_field() class Something { }
        r.Decorators.Add(new RtDecorator("with_dynamic_field()"));
        return r;        
    }
}

So basically you can change every aspect of AST node generated by RT. Keep in mind that nodes available via accessing Context.Location are also changeable. The example below suppreses output ot TypeScript class but rather adds all functions from class directly to namespace. It produces invalid code, of course, but we are just exploring opportunities, right?

using Reinforced.Typings.Generators;
using Reinforced.Typings.Ast;

public class FizzClassCodeGenerator : ClassCodeGenerator
{
    public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
    {
        // obtain current namespace
        var ns = this.Context.Location.CurrentNamespace;
        var r = base.GenerateNode(element, result, resolver);
        
        foreach (var rMember in r.Members)
        {
            var m = rMember as RtFuncion;
            if (m != null)
            {
                m.AccessModifier = null;

                ns.CompilationUnits.Add(m);
            }
        }

        // return null instead of result to 
        // suppress writing AST of original class 
        // to resulting file
        return null;      
    }
}

Use RtRaw

RtRaw is special AST that hold literally random text. You can add instances of RtRaw directly into namespaces, classes, interfaces or use it to implement methods. Below is example of generator using RtRaw do add corresponding Map<number,string> for generated enum (in order to restore enum names by values):

using Reinforced.Typings.Generators;
using Reinforced.Typings.Ast;

public class EnumWithMapGenerator : EnumGenerator
{
    public override RtEnum GenerateNode(Type element, RtEnum result, TypeResolver resolver)
    {
        // first we obtain generated enum AST node
        var resultEnum = base.GenerateNode(element, result, resolver);

        // check whether we are within namespace
        if (Context.Location.CurrentNamespace != null)
        {
            // add enum there (to ensure order)
            Context.Location.CurrentNamespace.CompilationUnits.Add(resultEnum);

            // construct piece of code
            StringBuilder enumdescriptor = new StringBuilder();
            enumdescriptor.AppendLine();

            // map initialization
            enumdescriptor.AppendLine($"const {resultEnum.EnumName} = new Map<number, string>([");
            
            // collect all enum values and put them into map along with their names
            bool first = true;            
            foreach (var resultEnumValue in resultEnum.Values)
            {
                if (!first) enumdescriptor.AppendLine(",");
                first = false;
                var enumDescription = resultEnumValue.EnumValueName; //<- here we get your desired enum description string somehow
                enumdescriptor.Append($"[{resultEnum.EnumName}.{resultEnumValue.EnumValueName},'{enumDescription}']");
            }
            enumdescriptor.AppendLine("]);");

            Context.Location.CurrentNamespace.CompilationUnits.Add(new RtRaw(enumdescriptor.ToString()));

        }

        return null;
    }
}

Another example: generator that produces AngularJS (old version) $http invokations out of controller methods (used along with special attribute):

using Reinforced.Typings.Generators;
using Reinforced.Typings.Attributes;
using Reinforced.Typings.Ast;

public class AngularMethodAttribute : TsFunctionAttribute
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="returnType"></param>
    public AngularMethodAttribute(Type returnType)
    {
        // Here we override method return type for TypeScript export
        StrongType = returnType;

        // Here we are specifying code generator for particular method
        CodeGeneratorType = typeof (AngularActionCallGenerator);
    }
}

public class AngularActionCallGenerator : MethodCodeGenerator
{
    public override RtFuncion GenerateNode(MethodInfo element, RtFuncion result, TypeResolver resolver)
    {

        result = base.GenerateNode(element, result, resolver);
        if (result == null) return null;

        // here we are overriding return type to corresponding promise
        var retType = result.ReturnType;
        bool isVoid = (retType is RtSimpleTypeName) && (((RtSimpleTypeName)retType).TypeName == "void");

        // we use TypeResolver to get "any" type to avoid redundant type name construction
        // (or because I'm too lazy to manually construct "any" type)
        if (isVoid) retType = resolver.ResolveTypeName(typeof(object));

        // Here we override TS method return type to make it angular.IPromise
        // We are using RtSimpleType with generig parameter of existing method type
        result.ReturnType = new RtSimpleTypeName(new[] { retType }, "angular", "IPromise");

        // Here we retrieve method parameters
        // We are using .GetName() extension method to retrieve parameter name
        // It is supplied within Reinforced.Typings and retrieves parameter name 
        // including possible name override with Fluent configuration or 
        // [TsParameter] attribute
        var p = element.GetParameters().Select(c => string.Format("'{0}': {0}", c.GetName()));

        // Joining parameters for method body code
        var dataParameters = string.Join(", ", p);

        // Here we get path to controller
        // It is quite simple solution requiring /{controller}/{action} route
        string controller = element.DeclaringType.Name.Replace("Controller", String.Empty);
        string path = String.Format("/{0}/{1}", controller, element.Name);

        const string code = @"var params = {{ {1} }};
return this.http.post('{0}', params)
.then((response) => {{ response.data['requestParams'] = params; return response.data; }});";

        RtRaw body = new RtRaw(String.Format(code, path, dataParameters));
        result.Body = body;

        // That's all. here we return node that will be written to target file.
        // Check result in /Scripts/ReinforcedTypings/GeneratedTypings.ts
        return result;
    }
}

Use Blueprints

Type blueprint is abstraction that encompasses all preferences regarding particular type export. There you can obtain:

  • Name for type/member (including the one that was overridden within configuration);
  • Type-wise substitutions;
  • Path to file where type must be located;
  • Track type/member ignorance
  • Obtain exporting configuration for type/members

And other useful information. To be honest, it is difficult to provide particular practically useful example of type blueprint usage, but it is important to notice that if you need some configuration information regarding exporting type or member - you have to look into type's blueprint.

When overriding type/member code generators - you easily can access current type's blueprint by using Context.CurrentBlueprint. Or, you can utilize Context.ProjectBlueprint in order to obtain blueprint for type that you are interested in. Besides of all, blueprints has pretty detailed XMLDOC - so you can easily understand what is what.

Use Documentation manager

Documentation manager is accessible by Context.Documentation. Actually there is XML parser that loads and caches XMLDOC for types being exported. And you have access to it by invoking Context.Documentation.GetDocumentationMember(...). There you can supply type or member that you'd like to obtain XMLDOC for. This method returns DocumentationMember entity that has few properties corresponding to standard XMLDOC tags like Parameters, Summary, Returns etc.

You can use this information however you want. And RT by default converts DocumentationMember into RtJsdocNode AST node that is accessible within each type/member AST via Documentation field.

How do I use custom visitor?

If you want to change the way that generated AST nodes are being written into file - you can inherit TextExportingVisitor or one of its children: TypeScriptExportVisitor or TypingsExportVisitor and override corresponding method:

using Reinforced.Typings.Visitors.TypeScript;

public class SampleVisitor : TypeScriptExportVisitor
{
    public DefaultExportsVisitor(TextWriter writer, ExportContext exportContext) : base(writer, exportContext)
    {
    }


    public override void Visit(RtEnumValue node)
    {
        // add comment to each exported enum
        WriteLines(@"
// This is enum!
");
        base.Visit(node);
    }

    public override void VisitFile(ExportedFile file)
    {
        // add "export = <first namespace>;" to the end of each generated file
        base.VisitFile(file);
        var ns = file.Namespaces.FirstOrDefault();
        if (ns != null)
        {
            WriteLines($@"
export = {ns.Name};
");
        }
    }
}

Then, point RT to use your visitor either by

[assembly:TsGlobal(VisitorType = typeof(SampleVisitor))]

or by fluent call:

public static void ConfigureTypings(ConfigurationBuilder builder)
{
    builder.Global(x=>x.UseVisitor<SampleVisitor>());
}
Clone this wiki locally