Skip to content

Instantly share code, notes, and snippets.

@alexdresko
Last active February 25, 2017 22:56
Show Gist options
  • Save alexdresko/5393155 to your computer and use it in GitHub Desktop.
Save alexdresko/5393155 to your computer and use it in GitHub Desktop.
T4 template to generate TypeScript interface definitions with BreezeJS support.
<#@ template language="C#" debug="true" hostspecific="true" #>
<#@ output extension=".d.ts" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ Include File="T4TS.tt.settings.t4" #>
<#=
OutputFormatter.GetOutput(GetDataToRender()) #><#+
List<TypeScriptModule> GetDataToRender() {
DTE dte = null;
// Get the DTE service from the host
var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null)
dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
// Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe
if (dte == null)
throw new Exception("Can only execute through the Visual Studio host");
var project = GetProjectContainingT4File(dte);
if (project == null)
throw new Exception("Could not find the VS project containing the T4TS file.");
// Read settings from T4TS.tt.settings.tt
var settings = new Settings
{
DefaultModule = DefaultModule,
DefaultOptional = DefaultOptional,
DefaultCamelCaseMemberNames = DefaultCamelCaseMemberNames,
DefaultInterfaceNamePrefix = DefaultInterfaceNamePrefix
};
var generator = new CodeTraverser(project, settings);
return generator.GetAllInterfaces().ToList();
}
Project GetProjectContainingT4File(DTE dte) {
// Find the .tt file's ProjectItem
ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
// If the .tt file is not opened, open it
if (projectItem.Document == null)
projectItem.Open(Constants.vsViewKindCode);
// Mark the .tt file as unsaved. This way it will be saved and update itself next time the
// project is built. Basically, it keeps marking itself as unsaved to make the next build work.
// Note: this is certainly hacky, but is the best I could come up with so far.
projectItem.Document.Saved = false;
return projectItem.ContainingProject;
}
public class CodeTraverser
{
public Project Project { get; private set; }
public Settings Settings { get; private set; }
private static readonly string InterfaceAttributeFullName = "T4TS.TypeScriptInterfaceAttribute";
private static readonly string MemberAttributeFullName = "T4TS.TypeScriptMemberAttribute";
public CodeTraverser(Project project, Settings settings)
{
if (project == null)
throw new ArgumentNullException("project");
if (settings == null)
throw new ArgumentNullException("settings");
this.Project = project;
this.Settings = settings;
}
public TypeContext BuildContext()
{
var typeContext = new TypeContext();
new ProjectTraverser(this.Project, (ns) =>
{
new NamespaceTraverser(ns, (codeClass) =>
{
CodeAttribute attribute;
if (!TryGetAttribute(codeClass.Attributes, InterfaceAttributeFullName, out attribute))
return;
var values = GetInterfaceValues(codeClass, attribute);
var customType = new CustomType(GetInterfaceName(values), values.Module);
typeContext.AddCustomType(codeClass.FullName, customType);
});
});
return typeContext;
}
public IEnumerable<TypeScriptModule> GetAllInterfaces()
{
var typeContext = BuildContext();
var byModuleName = new Dictionary<string, TypeScriptModule>();
var tsMap = new Dictionary<CodeClass, TypeScriptInterface>();
new ProjectTraverser(this.Project, (ns) =>
{
new NamespaceTraverser(ns, (codeClass) =>
{
if (codeClass.Attributes == null || codeClass.Attributes.Count == 0)
return;
CodeAttribute attribute;
if (!TryGetAttribute(codeClass.Attributes, InterfaceAttributeFullName, out attribute))
return;
var values = GetInterfaceValues(codeClass, attribute);
TypeScriptModule module;
if (!byModuleName.TryGetValue(values.Module, out module))
{
module = new TypeScriptModule { QualifiedName = values.Module };
byModuleName.Add(values.Module, module);
}
var tsInterface = BuildInterface(codeClass, values, typeContext);
tsMap.Add(codeClass, tsInterface);
tsInterface.Module = module;
module.Interfaces.Add(tsInterface);
});
});
var tsInterfaces = tsMap.Values.ToList();
tsMap.Keys.ToList().ForEach(codeClass =>
{
var parent = tsInterfaces.LastOrDefault(intf => codeClass.IsDerivedFrom[intf.FullName] && intf.FullName != codeClass.FullName);
if (parent != null)
tsMap[codeClass].Parent = parent;
});
return byModuleName.Values
.OrderBy(m => m.QualifiedName)
.ToList();
}
private string GetInterfaceName(TypeScriptInterfaceAttributeValues attributeValues)
{
if (!string.IsNullOrEmpty(attributeValues.NamePrefix))
return attributeValues.NamePrefix + attributeValues.Name;
return attributeValues.Name;
}
private TypeScriptInterface BuildInterface(CodeClass codeClass, TypeScriptInterfaceAttributeValues attributeValues, TypeContext typeContext)
{
var tsInterface = new TypeScriptInterface
{
FullName = codeClass.FullName,
Name = GetInterfaceName(attributeValues)
};
TypescriptType indexedType;
if (TryGetIndexedType(codeClass, typeContext, out indexedType))
tsInterface.IndexedType = indexedType;
new ClassTraverser(codeClass, (property) =>
{
TypeScriptInterfaceMember member;
if (TryGetMember(property, typeContext, out member))
tsInterface.Members.Add(member);
});
return tsInterface;
}
private bool TryGetAttribute(CodeElements attributes, string attributeFullName, out CodeAttribute attribute)
{
foreach (CodeAttribute attr in attributes)
{
if (attr.FullName == attributeFullName)
{
attribute = attr;
return true;
}
}
attribute = null;
return false;
}
private bool TryGetIndexedType(CodeClass codeClass, TypeContext typeContext, out TypescriptType indexedType)
{
indexedType = null;
if (codeClass.Bases == null || codeClass.Bases.Count == 0)
return false;
foreach (CodeElement baseClass in codeClass.Bases)
{
if (typeContext.IsGenericEnumerable(baseClass.FullName))
{
string fullName = typeContext.UnwrapGenericType(baseClass.FullName);
indexedType = typeContext.GetTypeScriptType(fullName);
return true;
}
}
return false;
}
private TypeScriptInterfaceAttributeValues GetInterfaceValues(CodeClass codeClass, CodeAttribute interfaceAttribute)
{
var values = GetAttributeValues(interfaceAttribute);
return new TypeScriptInterfaceAttributeValues
{
Name = values.ContainsKey("Name") ? values["Name"] : codeClass.Name,
Module = values.ContainsKey("Module") ? values["Module"] : Settings.DefaultModule ?? "T4TS",
NamePrefix = values.ContainsKey("NamePrefix") ? values["NamePrefix"] : Settings.DefaultInterfaceNamePrefix ?? string.Empty
};
}
private bool TryGetMember(CodeProperty property, TypeContext typeContext, out TypeScriptInterfaceMember member)
{
member = null;
if (property.Access != vsCMAccess.vsCMAccessPublic)
return false;
var getter = property.Getter;
if (getter == null)
return false;
var values = GetMemberValues(property, typeContext);
member = new TypeScriptInterfaceMember
{
Name = values.Name ?? property.Name,
FullName = property.FullName,
Optional = values.Optional,
Type = (string.IsNullOrWhiteSpace(values.Type))
? typeContext.GetTypeScriptType(getter.Type)
: new CustomType(values.Type)
};
if (values.CamelCase && values.Name == null)
member.Name = member.Name.Substring(0, 1).ToLowerInvariant() + member.Name.Substring(1);
return true;
}
private TypeScriptMemberAttributeValues GetMemberValues(CodeProperty property, TypeContext typeContext)
{
bool? attributeOptional = null;
bool? attributeCamelCase = null;
string attributeName = null;
string attributeType = null;
CodeAttribute attribute;
if (TryGetAttribute(property.Attributes, MemberAttributeFullName, out attribute))
{
var values = GetAttributeValues(attribute);
if (values.ContainsKey("Optional"))
attributeOptional = values["Optional"] == "true";
if (values.ContainsKey("CamelCase"))
attributeCamelCase = values["CamelCase"] == "true";
values.TryGetValue("Name", out attributeName);
values.TryGetValue("Type", out attributeType);
}
return new TypeScriptMemberAttributeValues
{
Optional = attributeOptional.HasValue ? attributeOptional.Value : Settings.DefaultOptional,
Name = attributeName,
Type = attributeType,
CamelCase = attributeCamelCase ?? Settings.DefaultCamelCaseMemberNames
};
}
private Dictionary<string, string> GetAttributeValues(CodeAttribute codeAttribute)
{
var values = new Dictionary<string, string>();
foreach (CodeElement child in codeAttribute.Children)
{
var property = (EnvDTE80.CodeAttributeArgument)child;
if (property == null || property.Value == null)
continue;
// remove quotes if the property is a string
string val = property.Value ?? string.Empty;
if (val.StartsWith("\"") && val.EndsWith("\""))
val = val.Substring(1, val.Length - 2);
values.Add(property.Name, val);
}
return values;
}
}
public class TypeContext
{
private static readonly string[] genericCollectionTypeStarts = new string[] {
"System.Collections.Generic.List<",
"System.Collections.Generic.IList<",
"System.Collections.Generic.ICollection<"
};
private static readonly string nullableTypeStart = "System.Nullable<";
/// <summary>
/// Lookup table for "custom types", ie. non-builtin types. Keyed on the FullName of the type.
/// </summary>
private Dictionary<string, CustomType> customTypes = new Dictionary<string, CustomType>();
public void AddCustomType(string typeFullName, CustomType customType)
{
customTypes.Add(typeFullName, customType);
}
public bool TryGetCustomType(string typeFullName, out CustomType customType)
{
return customTypes.TryGetValue(typeFullName, out customType);
}
public TypescriptType GetTypeScriptType(CodeTypeRef codeType)
{
switch (codeType.TypeKind)
{
case vsCMTypeRef.vsCMTypeRefChar:
case vsCMTypeRef.vsCMTypeRefString:
return new StringType();
case vsCMTypeRef.vsCMTypeRefBool:
return new BoolType();
case vsCMTypeRef.vsCMTypeRefByte:
case vsCMTypeRef.vsCMTypeRefDouble:
case vsCMTypeRef.vsCMTypeRefInt:
case vsCMTypeRef.vsCMTypeRefShort:
case vsCMTypeRef.vsCMTypeRefFloat:
case vsCMTypeRef.vsCMTypeRefLong:
case vsCMTypeRef.vsCMTypeRefDecimal:
return new NumberType();
default:
return TryResolveType(codeType);
}
}
private TypescriptType TryResolveType(CodeTypeRef codeType)
{
if (codeType.TypeKind == vsCMTypeRef.vsCMTypeRefArray)
{
return new ArrayType()
{
ElementType = GetTypeScriptType(codeType.ElementType)
};
}
return GetTypeScriptType(codeType.AsFullName);
}
private ArrayType TryResolveEnumerableType(string typeFullName)
{
return new ArrayType
{
ElementType = GetTypeScriptType(typeFullName)
};
}
public TypescriptType GetTypeScriptType(string typeFullName)
{
CustomType customType;
if (customTypes.TryGetValue(typeFullName, out customType))
return customType;
if (IsGenericEnumerable(typeFullName))
{
return new ArrayType
{
ElementType = GetTypeScriptType(UnwrapGenericType(typeFullName))
};
}
else if (IsNullable(typeFullName))
{
return new NullableType
{
WrappedType = GetTypeScriptType(UnwrapGenericType(typeFullName))
};
}
switch (typeFullName)
{
case "System.Double":
case "System.Int16":
case "System.Int32":
case "System.Int64":
case "System.UInt16":
case "System.UInt32":
case "System.UInt64":
case "System.Decimal":
case "System.Byte":
case "System.SByte":
case "System.Single":
return new NumberType();
case "System.String":
case "System.DateTime":
return new StringType();
default:
return new TypescriptType();
}
}
private bool IsNullable(string typeFullName)
{
return typeFullName.StartsWith(nullableTypeStart);
}
public string UnwrapGenericType(string typeFullName)
{
int firstIndex = typeFullName.IndexOf('<');
return typeFullName.Substring(firstIndex+1, typeFullName.Length - firstIndex- 2);
}
public bool IsGenericEnumerable(string typeFullName)
{
return genericCollectionTypeStarts.Any(t => typeFullName.StartsWith(t));
}
}
public class InterfaceOutputAppender : OutputAppender<TypeScriptInterface>
{
private bool InGlobalModule { get; set; }
public InterfaceOutputAppender(StringBuilder output, int baseIndentation, bool inGlobalModule)
: base(output, baseIndentation)
{
this.InGlobalModule = inGlobalModule;
}
public override void AppendOutput(TypeScriptInterface tsInterface)
{
BeginInterface(tsInterface);
AppendMembers(tsInterface);
if (tsInterface.IndexedType != null)
AppendIndexer(tsInterface);
EndInterface();
}
private void AppendMembers(TypeScriptInterface tsInterface)
{
var appender = new MemberOutputAppender(Output, BaseIndentation + 4);
foreach (var member in tsInterface.Members)
appender.AppendOutput(member);
}
private void BeginInterface(TypeScriptInterface tsInterface)
{
AppendIndentedLine("/** Generated from " + tsInterface.FullName + " **/");
if (InGlobalModule)
AppendIndented("interface " + tsInterface.Name);
else
AppendIndented("export interface " + tsInterface.Name);
if (tsInterface.Parent != null)
Output.Append(" extends " + (tsInterface.Parent.Module.IsGlobal ? "" : tsInterface.Parent.Module.QualifiedName + ".") + tsInterface.Parent.Name);
else
{
Output.Append(" extends breeze.Entity");
}
Output.AppendLine(" {");
}
private void EndInterface()
{
AppendIndentedLine("}");
}
private void AppendIndexer(TypeScriptInterface tsInterface)
{
AppendIndendation();
Output.AppendFormat(" [index: number]: {0};", tsInterface.IndexedType);
Output.AppendLine();
}
}
public class MemberOutputAppender : OutputAppender<TypeScriptInterfaceMember>
{
public MemberOutputAppender(StringBuilder output, int baseIndentation)
: base(output, baseIndentation)
{
}
public override void AppendOutput(TypeScriptInterfaceMember member)
{
AppendIndendation();
//bool isOptional = member.Optional || (member.Type is NullableType);
bool isOptional = true;
Output.AppendFormat("{0}(value{1}: {2}): {2}",
member.Name,
(isOptional ? "?" : ""),
member.Type
);
Output.AppendLine(";");
}
}
public class ModuleOutputAppender : OutputAppender<TypeScriptModule>
{
public ModuleOutputAppender(StringBuilder output, int baseIndentation)
: base(output, baseIndentation)
{
}
public override void AppendOutput(TypeScriptModule module)
{
BeginModule(module);
var interfaceAppender = new InterfaceOutputAppender(Output, BaseIndentation + 4, module.IsGlobal);
foreach (var tsInterface in module.Interfaces)
interfaceAppender.AppendOutput(tsInterface);
EndModule(module);
}
private void BeginModule(TypeScriptModule module)
{
if (module.IsGlobal)
{
Output.AppendLine("// -- Begin global interfaces");
}
else
{
Output.Append("module ");
Output.Append(module.QualifiedName);
Output.AppendLine(" {");
}
}
private void EndModule(TypeScriptModule module)
{
if (module.IsGlobal)
Output.AppendLine("// -- End global interfaces");
else
Output.AppendLine("}");
}
}
public abstract class OutputAppender<TSegment> where TSegment: class
{
protected StringBuilder Output { get; private set; }
protected int BaseIndentation { get; private set; }
public OutputAppender(StringBuilder output, int baseIndentation)
{
if (output == null)
throw new ArgumentNullException("output");
this.Output = output;
this.BaseIndentation = baseIndentation;
}
public abstract void AppendOutput(TSegment segment);
protected void AppendIndented(string text)
{
AppendIndendation();
Output.Append(text);
}
protected void AppendIndentedLine(string line)
{
AppendIndendation();
Output.AppendLine(line);
}
protected void AppendIndendation()
{
Output.Append(' ', BaseIndentation);
}
public override string ToString()
{
return Output.ToString();
}
}
public static class OutputFormatter
{
public static string GetOutput(List<TypeScriptModule> modules)
{
var output = new StringBuilder();
output.AppendLine("/****************************************************************************");
output.AppendLine(" Generated by T4TS.tt - don't make any changes in this file");
output.AppendLine("****************************************************************************/");
var moduleAppender = new ModuleOutputAppender(output, 0);
foreach (var module in modules)
{
output.AppendLine();
moduleAppender.AppendOutput(module);
}
return output.ToString();
}
}
public class Settings
{
/// <summary>
/// The default module of the generated interface, if not specified by the TypeScriptInterfaceAttribute
/// </summary>
public string DefaultModule { get; set; }
/// <summary>
/// The default value for Optional, if not specified by the TypeScriptMemberAttribute
/// </summary>
public bool DefaultOptional { get; set; }
/// <summary>
/// The default value for the CamelCase flag for an interface member name, if not specified by the TypeScriptMemberAttribute
/// </summary>
public bool DefaultCamelCaseMemberNames { get; set; }
/// <summary>
/// The default string to prefix interface names with. For instance, you might want to prefix the names with an "I" to get conventional interface names.
/// </summary>
public string DefaultInterfaceNamePrefix { get; set; }
}
public class TypeScriptInterface
{
public string Name { get; set; }
public string FullName { get; set; }
public List<TypeScriptInterfaceMember> Members { get; set; }
public TypescriptType IndexedType { get; set; }
public TypeScriptInterface Parent { get; set; }
public TypeScriptModule Module { get; set; }
public TypeScriptInterface()
{
Members = new List<TypeScriptInterfaceMember>();
}
}
public class TypeScriptInterfaceAttributeValues
{
public string Module { get; set; }
public string Name { get; set; }
public string NamePrefix { get; set; }
}
public class TypeScriptInterfaceMember
{
public string Name { get; set; }
public TypescriptType Type { get; set; }
public bool Optional { get; set; }
public string FullName { get; set; }
}
public class TypeScriptMemberAttributeValues
{
public string Name { get; set; }
public bool Optional { get; set; }
public string Type { get; set; }
public bool CamelCase { get; set; }
}
public class TypeScriptModule
{
public string QualifiedName { get; set; }
public List<TypeScriptInterface> Interfaces { get; set; }
/// <summary>
/// Returns true if this is the global namespace (ie. no module name)
/// </summary>
public bool IsGlobal
{
get { return string.IsNullOrWhiteSpace(QualifiedName); }
}
public TypeScriptModule()
{
Interfaces = new List<TypeScriptInterface>();
}
}
public class ClassTraverser
{
public CodeClass CodeClass { get; private set; }
public Action<CodeProperty> WithProperty { get; set; }
public ClassTraverser(CodeClass codeClass, Action<CodeProperty> withProperty)
{
if (codeClass == null)
throw new ArgumentNullException("codeClass");
if (withProperty == null)
throw new ArgumentNullException("withProperty");
this.CodeClass = codeClass;
this.WithProperty = withProperty;
if (codeClass.Members != null)
Traverse(codeClass.Members);
}
private void Traverse(CodeElements members)
{
foreach (var property in members.OfType<CodeProperty>())
WithProperty(property);
}
}
public class NamespaceTraverser
{
public Action<CodeClass> WithCodeClass { get; private set; }
public NamespaceTraverser(CodeNamespace ns, Action<CodeClass> withCodeClass)
{
if (ns == null)
throw new ArgumentNullException("ns");
if (withCodeClass == null)
throw new ArgumentNullException("withCodeClass");
WithCodeClass = withCodeClass;
if (ns.Members != null)
Traverse(ns.Members);
}
private void Traverse(CodeElements members)
{
foreach (var codeClass in members.OfType<CodeClass>())
WithCodeClass(codeClass);
}
}
public class ProjectTraverser
{
public Action<CodeNamespace> WithNamespace { get; private set; }
public ProjectTraverser(Project project, Action<CodeNamespace> withNamespace)
{
if (project == null)
throw new ArgumentNullException("project");
if (withNamespace == null)
throw new ArgumentNullException("withNamespace");
WithNamespace = withNamespace;
if (project.ProjectItems != null)
Traverse(project.ProjectItems);
}
private void Traverse(ProjectItems items)
{
foreach (ProjectItem pi in items)
{
if (pi.FileCodeModel != null)
{
var codeElements = pi.FileCodeModel.CodeElements;
foreach (var ns in codeElements.OfType<CodeNamespace>())
WithNamespace(ns);
}
if (pi.ProjectItems != null)
Traverse(pi.ProjectItems);
}
}
}
public class ArrayType: TypescriptType
{
public TypescriptType ElementType { get; set; }
public override string ToString()
{
return ElementType.ToString() + "[]";
}
}
public class BoolType: TypescriptType
{
public override string Name
{
get { return "bool"; }
}
}
public class CustomType: TypescriptType
{
private string m_name;
public override string Name
{
get { return m_name; }
}
public string QualifedModule { get; private set; }
public CustomType(string name, string qualifiedModule=null)
{
m_name = name;
this.QualifedModule = qualifiedModule;
}
public override string ToString()
{
if (string.IsNullOrWhiteSpace(QualifedModule))
return base.ToString();
return QualifedModule + "." + base.ToString();
}
}
public class NullableType : TypescriptType
{
public TypescriptType WrappedType { get; set; }
public override string ToString()
{
return WrappedType.ToString();
}
}
public class NumberType : TypescriptType
{
public override string Name
{
get { return "number"; }
}
}
public class StringType: TypescriptType
{
public override string Name
{
get { return "string"; }
}
}
public class TypescriptType
{
public virtual string Name { get { return "any"; } }
public override string ToString()
{
return Name;
}
}
#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment