Skip to content

Instantly share code, notes, and snippets.

@Fasteroid
Last active January 28, 2025 17:58
Show Gist options
  • Save Fasteroid/217e8f2b2784ae2f4f8a63be108fe29e to your computer and use it in GitHub Desktop.
Save Fasteroid/217e8f2b2784ae2f4f8a63be108fe29e to your computer and use it in GitHub Desktop.
The ultimate Typewriter template for your MVC controllers!
${
using Typewriter.Extensions.Types;
using System.Text.RegularExpressions;
using System.Reflection;
/*
__TypewriterControllers.tst v1.1.1 - https://gist.github.com/Fasteroid
Features:
- Automatically imports type dependencies from the same assembly
- Handles generics and arrays
- Handles nullable types
- Leaves casing alone
- Nullables are translated as `?` instead of `| null`
- Developer can implement `trimmed` to reduce bandwidth usage
- Developer can implement APIUtils to wrap API calls
*/
static string AssemblyName = "my-api";
string MethodPath(Method m){
String path = (String) m.Attributes.First(a => a.Name.StartsWith("Http")).Arguments.First().Value;
if( path == "[action]" ){
path = m.name;
}
return path;
}
string ClassPath(Class c){
String path = (String) c.Attributes.First(a => a.Name.StartsWith("Route")).Arguments.First().Value;
if( path == "[controller]" ){
path = c.Name.Replace("Controller","");
}
return path;
}
string ClassWithGenerics(Class c){
return ( (Type) c ).Name;
}
string TypeToString(Type t){
var ret = t.Name;
return ret;
}
Type ReturnType(Method m){
Type t = m.Type;
if( t.Name.StartsWith("ActionResult") ){
t = t.TypeArguments.FirstOrDefault();
}
return t;
}
string ReturnTypeAsString(Method m){
return TypeToString( ReturnType(m) );
}
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) );
}
return meta.Where( IsIncludable ).ToList();
}
bool IsIncludable(Type t){
return (t.AssemblyName == AssemblyName) && (t.ContainingClass == null); // no containing class = not a generic placeholder
}
IEnumerable<Type> GetIncludableTypes( IEnumerable<Type> types, Class parent ){
return types.SelectMany(t => GetMetaClasses(t))
.Where(t => IsIncludable(t))
.Where(t => OriginalName(t) != parent.Name);
}
string LastNPaths(string path, int n){
return string.Join("\\", path.Split('\\').Reverse().Take(n).Reverse());
}
string FileSpecificImports(File f){
List<Type> includes = new List<Type>();
foreach( Class c in f.Classes ){
var validMethods = c.Methods.Where( m => MethodType(m) != "unknown" );
includes.AddRange( GetIncludableTypes( validMethods.Select( ReturnType ).Where( t => t != null ), c ) );
includes.AddRange( GetIncludableTypes( validMethods.SelectMany(m => m.Parameters).Select(p => p.Type), c ) );
includes.AddRange( GetIncludableTypes( c.Properties.Select(p => p.Type), c ) );
}
return string.Join(Environment.NewLine, includes.Select((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("\\","/")}';";
}).Distinct());
}
string APIPath( Method m ){
Class c = (Class) m.Parent;
return $"/{ClassPath(c)}/{MethodPath(m)}";
}
string MethodType( Method m ){
if( m.Attributes.Any(a => a.Name.StartsWith("HttpGet")) ){
return "get";
}
if( m.Attributes.Any(a => a.Name.StartsWith("HttpPost")) ){
return "post";
}
return "unknown";
}
bool TypewriterIgnore( Method m ){
return m.Attributes.Any(a => a.Name == "TypewriterIgnore");
}
string ParamType( Method m ){
try {
return m.Parameters.First().Type.Name;
}
catch {
return "void";
}
}
void CollectTypeProperties( Type t, List<string> props ){
props.AddRange( t.Properties.Where(p => p.Attributes.All(a => a.Name != "JsonIgnore")).Select( p => "'" + p.Name + "'" ).ToList() );
if( t.BaseClass != null ){
CollectTypeProperties(t.BaseClass, props);
}
}
string ParamProperties( Method m ){
try {
var t = m.Parameters.First();
List<string> props = new List<string>();
CollectTypeProperties(t.Type, props);
return "[" + props.Aggregate((a, b) => a + ", " + b) + "]";
} catch {
return "[ /* Dear dev, this POST has no arguments. Please use a GET instead. */ ]";
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// 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/Fasteroid/Typewriter/ //
/////////////////////////////////////////////////////////////////////////////////////////////////////
import { Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { APIUtils } from 'services/apiutils.service';
import { trimmed } from 'shared/misc/Utils';
import { Observable } from 'rxjs';
$FileSpecificImports
const BASE = environment.apiUrlBase;
$Classes(:CustomControllerBase)[
@Injectable({
providedIn: 'root'
})
export class $Name {
constructor( private utils: APIUtils ) { }
$Methods(m => MethodType(m) == "get" && TypewriterIgnore(m) == false)[
$name(): Observable<$ReturnTypeAsString> {
return this.utils.get<$ReturnTypeAsString>(`${BASE}$APIPath`);
}]
$Methods(m => MethodType(m) == "post" && TypewriterIgnore(m) == false)[
$name(body: $ParamType): Observable<$ReturnTypeAsString> {
return this.utils.post<$ReturnTypeAsString>(`${BASE}$APIPath`, trimmed(body, $ParamProperties));
}]
]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment