Skip to content

Instantly share code, notes, and snippets.

@Hangsolow
Last active November 9, 2022 16:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f to your computer and use it in GitHub Desktop.
Save Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f to your computer and use it in GitHub Desktop.
public class ContentTypeCodeGenerator
{
private static readonly IDictionary<Type, Func<Type, PropertyInfo, string>> TypeMappings = CreateTypeMapping();
private static readonly IEnumerable<string> KeyWords = new[] { "Url" };
public ContentTypeCodeGenerator(string filePath)
{
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
}
private ILogger Logger { get; } = Log.ForContext<ContentTypeCodeGenerator>();
public string FilePath { get; }
public string EnumAssemblyNameFilter => "Almbrand";
private void GenerateTypescriptEnums(IEnumerable<Type> types, StringBuilder builder)
{
foreach (var type in types.Where(t => t.AssemblyQualifiedName.StartsWith(EnumAssemblyNameFilter, StringComparison.OrdinalIgnoreCase)))
{
if (type.IsEnum && !type.IsGenericType && typeof(int).IsAssignableFrom(type.GetEnumUnderlyingType()))
{
builder.AppendLine($"export enum {type.Name} {{");
AddEnumMembers(type);
builder.AppendLine("}");
}
}
void AddEnumMembers(Type type)
{
foreach (var enumValue in type.GetEnumValues().Cast<int>().Distinct())
{
builder.AppendLine($" {type.GetEnumName(enumValue)}={enumValue},");
}
}
}
private void GenerateTypescriptInterfaces()
{
IEnumerable<Assembly> assemblies = GetAssemblies();
IEnumerable<Type> types = assemblies.SelectMany(a => GetTypesFromAssembly(a)).ToList();
var contentTypes = types.Where(t => t.GetCustomAttribute<ContentTypeAttribute>() != null && !typeof(IContentMedia).IsAssignableFrom(t));
StringBuilder builder = new StringBuilder();
builder.AppendLine("import { IContent, ContentLanguage, ContentReference } from './content'");
GenerateTypescriptEnums(types, builder);
foreach (var contentType in contentTypes)
{
Logger.Information("Adding {ContentType} as typescript interface", contentType.Name);
builder.AppendLine($"export interface {contentType.Name} extends IContent {{");
AddProperties(contentType);
builder.AppendLine("}");
}
var fileText = builder.ToString();
if (HasFileContentChanged(fileText))
{
File.WriteAllText(FilePath, fileText);
}
IEnumerable<Assembly> GetAssemblies()
{
var rootAssembly = Assembly.GetAssembly(typeof(ContentTypeCodeGenerator));
yield return rootAssembly;
foreach (var assemblyName in rootAssembly.GetReferencedAssemblies())
{
yield return Assembly.Load(assemblyName);
}
}
IEnumerable<Type> GetTypesFromAssembly(Assembly assembly)
{
try
{
return assembly.GetExportedTypes();
}
catch (FileLoadException fileNotFoundException)
{
Logger.Warning($"Could not load types from assembly: {assembly.FullName} because it could not be found", fileNotFoundException);
return Enumerable.Empty<Type>();
}
}
void AddProperties(Type contentType)
{
foreach (PropertyInfo property in GetContentTypeProperties(contentType))
{
//camel cases the property name
var propertyName = char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1);
builder.AppendLine($" {propertyName}: {GetDataType(contentType, property)};");
}
}
string GetDataType(Type contentType, PropertyInfo property)
{
if (TypeMappings.TryGetValue(property.PropertyType, out var func))
{
return func(contentType, property);
}
else
{
if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
return FindListType(contentType, property);
}
return property.PropertyType.Name;
}
}
string FindListType(Type contentType, PropertyInfo property)
{
if (typeof(IEnumerable<string>).IsAssignableFrom(property.PropertyType))
{
return "Array<string>";
}
return "Array<any>";
}
IEnumerable<PropertyInfo> GetContentTypeProperties(Type contentType)
{
return contentType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty)
.Where(property => !IsPropertyIgnored(property));
}
bool HasFileContentChanged(string fileContent)
{
var currentHash = CreateHashForFileContent(fileContent);
if (!File.Exists(FilePath))
{
SaveHashFile(currentHash);
return true;
}
if (TryGetHashFromFile(out var hash) && currentHash.Equals(hash, StringComparison.Ordinal))
{
return false;
}
SaveHashFile(currentHash);
return true;
}
bool TryGetHashFromFile(out string hash)
{
hash = null;
string hashPath = FilePath + ".hash";
if (File.Exists(hashPath))
{
hash = File.ReadAllText(hashPath);
}
return !string.IsNullOrEmpty(hash);
}
void SaveHashFile(string hash)
{
string hashPath = FilePath + ".hash";
File.WriteAllText(hashPath, hash);
}
string CreateHashForFileContent(string fileContent)
{
using (var md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(fileContent));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
private static IDictionary<Type, Func<Type, PropertyInfo, string>> CreateTypeMapping()
{
var mappingDictionary = new Dictionary<Type, Func<Type, PropertyInfo, string>>();
mappingDictionary.Add(typeof(string), (t, p) => "string");
mappingDictionary.Add(typeof(int), (t, p) => "number");
mappingDictionary.Add(typeof(int?), (t, p) => "number");
mappingDictionary.Add(typeof(decimal), (t, p) => "number");
mappingDictionary.Add(typeof(decimal?), (t, p) => "number");
mappingDictionary.Add(typeof(float), (t, p) => "number");
mappingDictionary.Add(typeof(float?), (t, p) => "number");
mappingDictionary.Add(typeof(double), (t, p) => "number");
mappingDictionary.Add(typeof(double?), (t, p) => "number");
mappingDictionary.Add(typeof(bool), (t, p) => "boolean");
mappingDictionary.Add(typeof(bool?), (t, p) => "boolean");
mappingDictionary.Add(typeof(DateTime), (t, p) => "string");
mappingDictionary.Add(typeof(DateTime?), (t, p) => "string");
mappingDictionary.Add(typeof(ContentReference), (t, p) => GetContentReferenceType(t, p));
mappingDictionary.Add(typeof(PageReference), (t, p) => "ContentReference");
mappingDictionary.Add(typeof(ContentArea), (t, p) => "Array<IContent>");
mappingDictionary.Add(typeof(LinkItemCollection), (t, p) => "Array<string>");
mappingDictionary.Add(typeof(PropertyContentReferenceList), (t, p) => "Array<IContent>");
mappingDictionary.Add(typeof(Url), (t, p) => "string");
mappingDictionary.Add(typeof(XhtmlString), (t, p) => "string");
return mappingDictionary;
string GetContentReferenceType(Type contentType, PropertyInfo property)
{
var uiHint = property.GetCustomAttribute<UIHintAttribute>();
if (uiHint?.UIHint == UIHint.Image)
{
return "string";
}
return "ContentReference";
}
}
private static bool IsPropertyIgnored(PropertyInfo property)
{
return !HasPublicGetterAndSetter(property) || property.DeclaringType.Assembly == typeof(IContent).Assembly || Attribute.IsDefined(property, typeof(IgnoreAttribute), true) || KeyWords.Contains(property.Name);
}
private static bool HasPublicGetterAndSetter(PropertyInfo property)
{
return property.GetGetMethod() != null && property.GetSetMethod() != null;
}
public void Init()
{
GenerateTypescriptInterfaces();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment