Skip to content

Instantly share code, notes, and snippets.

@Fasteroid
Last active January 28, 2025 17:47
Show Gist options
  • Save Fasteroid/71ece35783c21b34a7ba7f11cc62add4 to your computer and use it in GitHub Desktop.
Save Fasteroid/71ece35783c21b34a7ba7f11cc62add4 to your computer and use it in GitHub Desktop.
The ultimate Typewriter template for C# files that have models in them.
${
using Typewriter.VisualStudio;
static ILog console;
Template(Settings settings){
console = settings.Log;
}
/*
__TypewriterModels.tst v1.2.0 - https://gist.github.com/Fasteroid
Features:
- Automatically imports type dependencies from the same assembly
- Handles generics and primitive arrays
- Handles nullable types
- Ignores properties with [JsonIgnore]
- Leaves casing alone
- Nullables are translated as `?` instead of `| null`
- Supports overrides for system types in `IncludeOverrides`
- Now with console logging!
*/
static Dictionary<string, string> IncludeOverrides = new Dictionary<string, string> {
{ "FileContentResult", "Special/FileContentResult" },
{ "FileResult", "Special/FileContentResult" }
};
static string NamespaceToInclude = "my_api.Models";
static string AssemblyName = "my-api";
public static T ForceAccess<T>(object obj, string propertyName)
{
return (T) obj.GetType()
.GetProperty(propertyName)
.GetValue(obj);
}
string ClassWithGenerics(Class c){
return ( (Type) c ).Name;
}
string OriginalName(Type t){
return t.OriginalName.Replace("?","");
}
IEnumerable<Type> GetMetaClasses(Type t){
var meta = new List<Type>(){t};
if( t.IsGeneric || t.IsEnumerable ){
meta.AddRange( t.TypeArguments.SelectMany(GetMetaClasses) );
}
if( t.BaseClass != null ){
meta.AddRange( GetMetaClasses(t.BaseClass) );
}
return meta.Where( IsIncludable ).ToList();
}
bool IsIncludable(Type t){
return ( t.AssemblyName == AssemblyName || IncludeOverrides.ContainsKey(OriginalName(t)) ) & ( t.ContainingClass == null ); // no containing class = not a generic placeholder
}
IEnumerable<Type> GetIncludableTypes( IEnumerable<Type> types, File f ){
return types.SelectMany(t => GetMetaClasses(t))
.Where(t => IsIncludable(t))
.Where(t =>
!f.Classes.Any( c => c.Name == OriginalName(t) ) &&
!f.Enums.Any( e => e.Name == OriginalName(t) ) &&
!AutoInterfaces(f.Interfaces).Any( i => i.Name == OriginalName(t) )
);
}
string LastNPaths(string path, int n){
return string.Join("\\", path.Split('\\').Reverse().Take(n).Reverse());
}
string FileSpecificImports(File f){
List<Type> typeIncludes = new List<Type>();
List<string> fileIncludes = new List<string>();
foreach( Class c in f.Classes ){
typeIncludes.AddRange( GetIncludableTypes(
c.Properties
.Where( p => !p.Attributes.Any(a => a.Name.StartsWith("JsonIgnore")) )
.Select( p => p.Type ),
f
));
typeIncludes.AddRange( GetIncludableTypes(
AutoInterfaces(c.Interfaces)
.Select( i => i.Type ),
f
));
typeIncludes.AddRange( GetIncludableTypes( GetMetaClasses(c), f ) );
fileIncludes.AddRange( c.Properties
.Where( p => p.Attributes.Any(a => a.Name.StartsWith("JsonInject")) )
.Select( p => (String) p.Attributes.Where(a => a.Name.StartsWith("JsonInject")).First().Arguments.First().Value )
.Select( s => $"import {{ {s} }} from '../Special/{s}';")
);
}
foreach( Interface i in f.Interfaces ){
typeIncludes.AddRange( GetIncludableTypes(
i.Properties
.Where( p => !p.Attributes.Any(a => a.Name.StartsWith("JsonIgnore")) )
.Select(p => p.Type),
f
));
typeIncludes.AddRange( GetIncludableTypes( GetMetaClasses(i), f ) );
typeIncludes.AddRange( GetIncludableTypes(
AutoInterfaces(i.Interfaces)
.Select( i => i.Type ),
f
));
fileIncludes.AddRange( i.Properties
.Where( p => p.Attributes.Any(a => a.Name.StartsWith("JsonInject")) )
.Select( p => (String) p.Attributes.Where(a => a.Name.StartsWith("JsonInject")).First().Arguments.First().Value )
.Select( s => $"import {{ {s} }} from '../Special/{s}';")
);
}
fileIncludes.AddRange( typeIncludes.Select( (t) => {
if( IncludeOverrides.ContainsKey(OriginalName(t)) ){
return $"import {{ {OriginalName(t)} }} from '../{IncludeOverrides[OriginalName(t)]}';";
}
var path = LastNPaths( t.FileLocations.First(), 2 );
path = path.Replace("\\","/"); // fix for windows paths
path = path.Substring(0, path.LastIndexOf('.')); // remove file extension
return $"import {{ {OriginalName(t)} }} from '../{path.Replace("\\","/")}';";
}));
return String.Join( Environment.NewLine, fileIncludes.Distinct() );
}
bool IsNullable(Property p){
return p.Type.IsNullable;
}
string TypeNotNull(Property p){
var inject = p.Attributes.Where( a => a.Name.StartsWith("JsonInject") );
if( inject.Count() > 0 ){
return (String) inject.First().Arguments.First().Value; // lie, but actually tell the truth
}
string t = p.Type.ToString();
return t.Replace(" | null","");
}
string Optional(Property p){
return IsNullable(p) ? "?" : "";
}
string BaseType(Class c){
return c.BaseClass == null ? "" : $" {ClassWithGenerics(c.BaseClass)} & ";
}
string BaseInterfaces(Interface i){
return String.Join( "", i.Interfaces.Select( ii => $"{ii.Name} & " ) );
}
IEnumerable<Interface> AutoInterfaces(IInterfaceCollection those){
return those.Where( ii => ii.Attributes.Any( a => a.Name == "AutoImplementable" ) );
}
string AutoInterfacesList(Class c){
return String.Join( "", AutoInterfaces(c.Interfaces).Select( i => $"{i.Name} & " ) );
}
bool ShouldRenderInterface(Interface i){
return i.Namespace == NamespaceToInclude && i.Attributes.Any( a => a.Name == "AutoImplementable" );
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// WARNING: THIS IS AN AUTO-GENERATED FILE - DO NOT MODIFY IT! YOUR CHANGES WILL BE OVERWRITTEN! //
// MODIFY THE TEMPLATE (.tst) FILE INSTEAD! //
// https://github.com/AdaskoTheBeAsT/Typewriter //
/////////////////////////////////////////////////////////////////////////////////////////////////////
$FileSpecificImports
$Classes(c => c.Namespace == NamespaceToInclude)[
export type $ClassWithGenerics = $BaseType$AutoInterfacesList{ $Properties(p => !p.Attributes.Any(a => a.Name == "JsonIgnore"))[
$Name$Optional: $TypeNotNull;]
}
]
$Interfaces(i => ShouldRenderInterface(i))[
// Do not use directly.
export type $Name = $BaseInterfaces{ $Properties(p => !p.Attributes.Any(a => a.Name == "JsonIgnore"))[
$Name$Optional: $TypeNotNull;]
}
]
$Enums(e => e.Namespace == NamespaceToInclude)[
export enum $Name { $Values[
$Name = $Value,]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment