-
Notifications
You must be signed in to change notification settings - Fork 82
Code Generators
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.
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.
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:
-
ClassCodeGenerator: generates code for classes (
RtClass
AST) -
InterfaceCodeGenerator: generates code for classes (
RtInterface
AST) -
PropertyCodeGenerator and FieldCodeGenerator: generates code for both fields and properties (produces
RtField
AST) -
MethodCodeGenerator: generates code for methods (
RtFunction
AST) -
EnumGenerator: generates code for enums (
RtEnum
AST)
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 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.
Basic flow is:
- Inherit existing code generator that fits your abilities, customize it
- 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.
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
}
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>();
}
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)
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;
}
}
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;
}
}
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.
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.
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>());
}