Last active
November 9, 2022 16:01
-
-
Save Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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