2
0
mirror of https://github.com/esiur/esiur-dotnet.git synced 2026-04-04 12:28:21 +00:00
This commit is contained in:
2026-04-04 04:31:30 +03:00
parent 1339604bc5
commit 5f73cf7af7
298 changed files with 100 additions and 501 deletions

View File

@@ -0,0 +1,332 @@
// ================================
// FILE: ResourceIncrementalGenerator.cs
// Replaces: ResourceGenerator.cs + ResourceGeneratorReceiver.cs
// ================================
using Esiur.Core;
using Esiur.Data;
using Esiur.Data.Types;
using Esiur.Protocol;
using Esiur.Resource;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Esiur.Proxy
{
[Generator(LanguageNames.CSharp)]
public sealed class ResourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1) Discover candidate classes via a cheap syntax filter
var perClass = context.SyntaxProvider.CreateSyntaxProvider(
(node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,
(ctx, _) => AnalyzeClass(ctx)
)
.Where( x => x is not null)!;
// 2) Aggregate import URLs (distinct)
var importUrls = perClass
.SelectMany((x, y) => x.Value.ImportUrls)
.Collect()
.Select( (urls, y) => urls.Distinct(StringComparer.Ordinal).ToImmutableArray());
// 3) Aggregate class infos and merge partials by stable key
var mergedResources = perClass
.Select( (x, y) => x.Value.ClassInfo)
.Where( ci => ci is not null)!
.Select( (ci, y) => ci!)
.Collect()
.Select( (list, y) => MergePartials(list));
// 4) Generate: A) remote TypeDefs (from ImportAttribute URLs)
context.RegisterSourceOutput(importUrls, (spc, urls) =>
{
if (urls.Length == 0) return;
foreach (var path in urls)
{
try
{
if (!TypeDefGenerator.urlRegex.IsMatch(path))
continue;
var parts = TypeDefGenerator.urlRegex.Split(path);
var con = Warehouse.Default.Get<EpConnection>($"{parts[1]}://{parts[2]}").Wait(20000);
var typeDefs = con.GetLinkDefinitions(parts[3]).Wait(60000);
EmitTypeDefs(spc, typeDefs);
}
catch (Exception ex)
{
Report(spc, "Esiur", ex.Message, DiagnosticSeverity.Error);
}
}
});
// 4) Generate: B) per resource partials (properties/events, base IResource impl if needed)
context.RegisterSourceOutput(mergedResources, static (spc, classes) =>
{
foreach (var ci in classes)
{
try
{
var code = @$"using Esiur.Resource;
using Esiur.Core;
#nullable enable
namespace {ci.ClassSymbol.ContainingNamespace.ToDisplayString()} {{
";
if (IsInterfaceImplemented(ci, classes))
code += $"public partial class {ci.Name} {{\r\n";
else
{
code +=
$@" public partial class {ci.Name} : IResource {{
public virtual Instance? Instance {{ get; set; }}
public virtual event DestroyedEvent? OnDestroy;
public virtual void Destroy() {{ OnDestroy?.Invoke(this); }}
";
if (!ci.HasTrigger)
code += "\tpublic virtual AsyncReply<bool> Trigger(ResourceTrigger trigger) => new AsyncReply<bool>(true);\r\n\r\n";
}
foreach (var f in ci.Fields)
{
var givenName = f.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ExportAttribute")?.ConstructorArguments.FirstOrDefault().Value as string;
var fn = f.Name;
var pn = string.IsNullOrEmpty(givenName) ? SuggestExportName(fn) : givenName;
var attrs = string.Join("\r\n\t", f.GetAttributes().Select(x => FormatAttribute(x)));
if (f.Type.Name.StartsWith("ResourceEventHandler") || f.Type.Name.StartsWith("CustomResourceEventHandler"))
{
code += $"\t{attrs}\r\n\t public event {f.Type} {pn};\r\n";
}
else
{
code += $"\t{attrs}\r\n\t public {f.Type} {pn} {{ \r\n\t\t get => {fn}; \r\n\t\t set {{ \r\n\t\t this.{fn} = value; \r\n\t\t Instance?.Modified(); \r\n\t\t}}\r\n\t}}\r\n";
}
}
code += "}}\r\n";
spc.AddSource(ci.Name + ".g.cs", code);
}
catch (Exception ex)
{
spc.AddSource(ci.Name + ".Error.g.cs", $"/*\r\n{ex}\r\n*/");
}
}
});
}
// === Analysis ===
private static PerClass? AnalyzeClass(GeneratorSyntaxContext ctx)
{
var cds = (ClassDeclarationSyntax)ctx.Node;
var cls = ctx.SemanticModel.GetDeclaredSymbol(cds) as ITypeSymbol;
if (cls is null) return null;
var attrs = cls.GetAttributes();
// Collect ImportAttribute URLs
var importUrls = attrs
.Where(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ImportAttribute")
.SelectMany(a => a.ConstructorArguments.Select(x => x.Value?.ToString()))
.Where(s => !string.IsNullOrWhiteSpace(s))
.Cast<string>()
.ToImmutableArray();
// If class has ResourceAttribute, gather details
var hasResource = attrs.Any(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ResourceAttribute");
ResourceClassInfo? classInfo = null;
if (hasResource)
{
bool hasTrigger = cds.Members
.OfType<MethodDeclarationSyntax>()
.Select(m => ctx.SemanticModel.GetDeclaredSymbol(m) as IMethodSymbol)
.Where(s => s is not null)
.Any(s => s!.Name == "Trigger" && s.Parameters.Length == 1 && s.Parameters[0].Type.ToDisplayString() == "Esiur.Resource.ResourceTrigger");
var exportedFields = cds.Members
.OfType<FieldDeclarationSyntax>()
.SelectMany(f => f.Declaration.Variables.Select(v => (f, v)))
.Select(t => ctx.SemanticModel.GetDeclaredSymbol(t.v) as IFieldSymbol)
.Where(f => f is not null && !f!.IsConst)
.Where(f => f!.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ExportAttribute"))
.Cast<IFieldSymbol>()
.ToList();
bool hasInterface = cls.AllInterfaces.Any(x => x.ToDisplayString() == "Esiur.Resource.IResource");
var key = $"{cls.ContainingAssembly.Name}:{cls.ContainingNamespace.ToDisplayString()}.{cls.Name}";
classInfo = new ResourceClassInfo
(
key,
cls.Name,
cds,
cls,
exportedFields,
hasInterface,
hasTrigger
);
}
return new PerClass(classInfo, importUrls);
}
private static ImmutableArray<ResourceClassInfo> MergePartials(ImmutableArray<ResourceClassInfo> list)
{
var byKey = new Dictionary<string, ResourceClassInfo>(StringComparer.Ordinal);
foreach (var item in list)
{
if (byKey.TryGetValue(item.Key, out var existing))
{
// merge fields + flags
var mergedFields = existing.Fields.Concat(item.Fields).ToList();
byKey[item.Key] = existing with
{
Fields = mergedFields,
HasInterface = existing.HasInterface || item.HasInterface,
HasTrigger = existing.HasTrigger || item.HasTrigger
};
}
else
{
byKey[item.Key] = item with { Fields = item.Fields.ToList() };
}
}
return byKey.Values.ToImmutableArray();
}
// Determine if the base already implements IResource (either directly or via another generated part)
private static bool IsInterfaceImplemented(ResourceClassInfo ci, ImmutableArray<ResourceClassInfo> merged)
{
if (ci.HasInterface) return true;
var baseType = ci.ClassSymbol.BaseType;
if (baseType is null) return false;
var baseKey = $"{baseType.ContainingAssembly.Name}:{baseType.ContainingNamespace.ToDisplayString()}.{baseType.Name}";
return merged.Any(x => x.Key == baseKey);
}
// === Emission helpers (ported from your original generator) ===
private static void EmitTypeDefs(SourceProductionContext spc, TypeDef[] typeDefs)
{
foreach (var typeDef in typeDefs)
{
if (typeDef.Kind == TypeDefKind.Resource)
{
var source = TypeDefGenerator.GenerateClass(typeDef, typeDefs, false);
spc.AddSource(typeDef.Name + ".g.cs", source);
}
else if (typeDef.Kind == TypeDefKind.Record)
{
var source = TypeDefGenerator.GenerateRecord(typeDef, typeDefs);
spc.AddSource(typeDef.Name + ".g.cs", source);
}
}
var typesFile = "using System; \r\n namespace Esiur { public static class Generated { public static Type[] Resources {get;} = new Type[] { " +
string.Join(",", typeDefs.Where(x => x.Kind == TypeDefKind.Resource).Select(x => $"typeof({x.Name})"))
+ " }; \r\n public static Type[] Records { get; } = new Type[] { " +
string.Join(",", typeDefs.Where(x => x.Kind == TypeDefKind.Record).Select(x => $"typeof({x.Name})"))
+ " }; " +
"\r\n } \r\n}";
spc.AddSource("Esiur.g.cs", typesFile);
}
private static void Report(SourceProductionContext ctx, string title, string message, DiagnosticSeverity severity)
{
var descriptor = new DiagnosticDescriptor("ESIUR001", title, message, "Esiur", severity, true);
ctx.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None));
}
// === Formatting helpers from your original code ===
private static string SuggestExportName(string fieldName)
{
if (char.IsUpper(fieldName[0]))
return fieldName.Substring(0, 1).ToLowerInvariant() + fieldName.Substring(1);
else
return char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1);
}
private static string FormatAttribute(AttributeData attribute)
{
if (attribute.AttributeClass is null)
throw new Exception("AttributeClass not found");
var className = attribute.AttributeClass.ToDisplayString();
if (!attribute.ConstructorArguments.Any() & !attribute.NamedArguments.Any())
return $"[{className}]";
var sb = new StringBuilder();
sb.Append('[').Append(className).Append('(');
if (attribute.ConstructorArguments.Any())
sb.Append(string.Join(", ", attribute.ConstructorArguments.Select(FormatConstant)));
if (attribute.NamedArguments.Any())
{
if (attribute.ConstructorArguments.Any()) sb.Append(", ");
sb.Append(string.Join(", ", attribute.NamedArguments.Select(na => $"{na.Key} = {FormatConstant(na.Value)}")));
}
sb.Append(")] ");
return sb.ToString();
}
private static string FormatConstant(TypedConstant constant)
{
if (constant.Kind == TypedConstantKind.Array)
return $"new {constant.Type?.ToDisplayString()} {{{string.Join(", ", constant.Values.Select(FormatConstant))}}}";
return constant.ToCSharpString();
}
// === Data carriers for the pipeline ===
private readonly record struct PerClass {
public PerClass(ResourceClassInfo? classInfo, ImmutableArray<string> importUrls)
{
this.ImportUrls = importUrls;
this.ClassInfo = classInfo;
}
public readonly ImmutableArray<string> ImportUrls;
public readonly ResourceClassInfo? ClassInfo;
}
private sealed record ResourceClassInfo {
public ResourceClassInfo(string key, string name ,
ClassDeclarationSyntax classDeclaration,
ITypeSymbol classSymbol, List<IFieldSymbol> fileds, bool hasInterface, bool hasTrigger)
{
Key = key;
Name = name;
ClassDeclaration = classDeclaration;
ClassSymbol = classSymbol;
Fields = fileds;
HasInterface = hasInterface;
HasTrigger = hasTrigger;
}
public string Key;
public string Name;
public ClassDeclarationSyntax ClassDeclaration;
public ITypeSymbol ClassSymbol;
public List<IFieldSymbol> Fields;
public bool HasInterface;
public bool HasTrigger;
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Text;
namespace Esiur.Proxy;
public struct ResourceGeneratorClassInfo
{
public string Name { get; set; }
public bool HasInterface { get; set; }
public bool HasTrigger { get; set; }
public List<IFieldSymbol> Fields { get; set; }
public ITypeSymbol ClassSymbol { get; set; }
public ClassDeclarationSyntax ClassDeclaration { get; set; }
// Deprecated in incremental path. Use IsInterfaceImplemented(ResourceClassInfo, merged) instead.
public bool IsInterfaceImplemented(System.Collections.Generic.Dictionary<string, ResourceGeneratorClassInfo> classes) => HasInterface;
}

View File

@@ -0,0 +1,11 @@
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Text;
namespace Esiur.Proxy;
public struct ResourceGeneratorFieldInfo
{
public IFieldSymbol FieldSymbol { get; set; }
public string[] Attributes { get; set; }
}

View File

@@ -0,0 +1,103 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace Esiur.Proxy;
public class ResourceGeneratorReceiver : ISyntaxContextReceiver
{
public Dictionary<string, ResourceGeneratorClassInfo> Classes { get; } = new();
public List<string> Imports { get; } = new();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is ClassDeclarationSyntax)
{
var cds = context.Node as ClassDeclarationSyntax;
var cls = context.SemanticModel.GetDeclaredSymbol(cds) as ITypeSymbol;
var attrs = cls.GetAttributes();
var imports = attrs.Where(a => a.AttributeClass.ToDisplayString() == "Esiur.Resource.ImportAttribute");
foreach (var import in imports)
{
// Debugger.Launch();
var urls = import.ConstructorArguments.Select(x => x.Value.ToString());//.ToString();
foreach(var url in urls)
if (!Imports.Contains(url))
Imports.Add(url);
}
if (attrs.Any(a => a.AttributeClass.ToDisplayString() == "Esiur.Resource.ResourceAttribute"))
{
var hasTrigger = cds.Members
.Where(x => x is MethodDeclarationSyntax)
.Select(x => context.SemanticModel.GetDeclaredSymbol(x) as IMethodSymbol)
.Any(x => x.Name == "Trigger"
&& x.Parameters.Length == 1
&& x.Parameters[0].Type.ToDisplayString() == "Esiur.Resource.ResourceTrigger");
var fields = cds.Members.Where(x => x is FieldDeclarationSyntax)
.Select(x => context.SemanticModel.GetDeclaredSymbol((x as FieldDeclarationSyntax).Declaration.Variables.First()) as IFieldSymbol)
.Where(x => !x.IsConst)
.Where(x => x.GetAttributes().Any(a => a.AttributeClass.ToDisplayString() == "Esiur.Resource.ExportAttribute"))
.ToArray();
//if (!Debugger.IsAttached)
//{
// if (cls.Name == "MyChildResource")
// Debugger.Launch();
//}
// get fields
var fullName = cls.ContainingAssembly + "." + cls.Name;
// Partial class check
if (Classes.ContainsKey(fullName))
{
// append fields
var c = Classes[fullName];
c.Fields.AddRange(fields);
if (!c.HasInterface)
c.HasInterface = cls.AllInterfaces.Any(x => x.ToDisplayString() == "Esiur.Resource.IResource");
if (!c.HasTrigger)
c.HasTrigger = hasTrigger;
}
else
{
Classes.Add(fullName, new ResourceGeneratorClassInfo()
{
Name = cls.Name,
ClassDeclaration = cds,
ClassSymbol = cls,
Fields = fields.ToList(),
HasInterface = cls.AllInterfaces.Any(x => x.ToDisplayString() == "Esiur.Resource.IResource"),
HasTrigger = hasTrigger
});
}
return;
}
}
}
}

View File

@@ -0,0 +1,256 @@
using Esiur.Data;
using Esiur.Resource;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
namespace Esiur.Proxy;
public static class ResourceProxy
{
static Dictionary<Type, Type> cache = new Dictionary<Type, Type>();
#if NETSTANDARD
static MethodInfo modifyMethod = typeof(Instance).GetTypeInfo().GetMethod("Modified");
static MethodInfo instanceGet = typeof(IResource).GetTypeInfo().GetProperty("Instance").GetGetMethod();
#else
static MethodInfo modifyMethod = typeof(Instance).GetMethod("Modified");
static MethodInfo instanceGet = typeof(IResource).GetProperty("Instance").GetGetMethod();
#endif
public static Type GetBaseType(object resource)
{
return GetBaseType(resource.GetType());
}
public static Type GetBaseType(Type type)
{
if (type == null)
throw new NullReferenceException("Type can't be null.");
if (type.Assembly.IsDynamic)
return type.GetTypeInfo().BaseType;
else
return type;
// if (type.FullName.Contains("Esiur.Proxy.T"))
//#if NETSTANDARD
// return type.GetTypeInfo().BaseType;
//#else
// return type.BaseType;
//#endif
// else
// return type;
}
public static Type GetProxy(Type type)
{
if (cache.ContainsKey(type))
return cache[type];
// check if the type was made with code generation
if (type.GetCustomAttribute<ResourceAttribute>(false) != null)
{
cache.Add(type, type);
return type;
}
if (!Codec.ImplementsInterface(type, typeof(IResource)))
{
cache.Add(type, type);
return type;
}
#if NETSTANDARD
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsSealed || typeInfo.IsAbstract)
throw new Exception("Sealed/Abastract classes can't be proxied.");
var props = from p in typeInfo.GetProperties(BindingFlags.Instance | BindingFlags.Public)
where p.CanWrite && p.SetMethod.IsVirtual && !p.SetMethod.IsFinal &&
p.GetCustomAttribute<ExportAttribute>(false) != null
select p;
#else
if (type.IsSealed)
throw new Exception("Sealed class can't be proxied.");
var props = from p in type.GetProperties()
where p.CanWrite && p.GetSetMethod().IsVirtual &&
p.GetCustomAttributes(typeof(ExportAttribute), false).Count() > 0
select p;
#endif
var assemblyName = new AssemblyName("Esiur.Proxy.T." + type.Assembly.GetName().Name);// type.Namespace);
assemblyName.Version = type.Assembly.GetName().Version;
assemblyName.CultureInfo = type.Assembly.GetName().CultureInfo;
//assemblyName.SetPublicKeyToken(null);
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
var typeName = "Esiur.Proxy.T." + type.FullName;// Assembly.CreateQualifiedName(assemblyName.FullName, "Esiur.Proxy.T." + type.FullName);
var typeBuilder = moduleBuilder.DefineType(typeName,
TypeAttributes.Public | TypeAttributes.Class, type);
foreach (PropertyInfo propertyInfo in props)
CreateProperty(propertyInfo, typeBuilder, type);
#if NETSTANDARD
var t = typeBuilder.CreateTypeInfo().AsType();
cache.Add(type, t);
return t;
#else
var t = typeBuilder.CreateType();
cache.Add(type, t);
return t;
#endif
}
public static Type GetProxy<T>()
where T : IResource
{
return GetProxy(typeof(T));
}
//private static void C
private static void CreateProperty(PropertyInfo pi, TypeBuilder typeBuilder, Type resourceType)
{
var propertyBuilder = typeBuilder.DefineProperty(pi.Name, PropertyAttributes.None, pi.PropertyType, null);
// Create set method
MethodBuilder builder = typeBuilder.DefineMethod("set_" + pi.Name,
MethodAttributes.Public | MethodAttributes.Virtual, null, new Type[] { pi.PropertyType });
builder.DefineParameter(1, ParameterAttributes.None, "value");
ILGenerator g = builder.GetILGenerator();
var getInstance = resourceType.GetTypeInfo().GetProperty("Instance").GetGetMethod();
//g.Emit(OpCodes.Ldarg_0);
//g.Emit(OpCodes.Ldarg_1);
//g.Emit(OpCodes.Call, pi.GetSetMethod());
//g.Emit(OpCodes.Nop);
//g.Emit(OpCodes.Ldarg_0);
//g.Emit(OpCodes.Call, getInstance);
//g.Emit(OpCodes.Ldstr, pi.Name);
//g.Emit(OpCodes.Call, modifyMethod);
//g.Emit(OpCodes.Nop);
//g.Emit(OpCodes.Ret);
Label exitMethod = g.DefineLabel();
Label callModified = g.DefineLabel();
g.Emit(OpCodes.Nop);
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Ldarg_1);
g.Emit(OpCodes.Call, pi.GetSetMethod());
g.Emit(OpCodes.Nop);
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Call, getInstance);
g.Emit(OpCodes.Dup);
g.Emit(OpCodes.Brtrue_S, callModified);
g.Emit(OpCodes.Pop);
g.Emit(OpCodes.Br_S, exitMethod);
g.MarkLabel(callModified);
g.Emit(OpCodes.Ldstr, pi.Name);
g.Emit(OpCodes.Call, modifyMethod);
g.Emit(OpCodes.Nop);
g.MarkLabel(exitMethod);
g.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(builder);
builder = typeBuilder.DefineMethod("get_" + pi.Name, MethodAttributes.Public | MethodAttributes.Virtual, pi.PropertyType, null);
g = builder.GetILGenerator();
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Call, pi.GetGetMethod());
g.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(builder);
// g.Emit(OpCodes.Ldarg_0);
// g.Emit(OpCodes.Call, pi.GetGetMethod());
// g.Emit(OpCodes.Ret);
// propertyBuilder.SetGetMethod(builder);
/*
Label callModified = g.DefineLabel();
Label exitMethod = g.DefineLabel();
// IL_0000: ldarg.0
//IL_0001: call instance class [Esiur]Esiur.Resource.Instance [Esiur]Esiur.Resource.Resource::get_Instance()
//// (no C# code)
//IL_0006: dup
//IL_0007: brtrue.s IL_000c
//IL_0009: pop
//// }
//IL_000a: br.s IL_0017
//// (no C# code)
//IL_000c: ldstr "Level3"
//IL_0011: call instance void [Esiur]Esiur.Resource.Instance::Modified(string)
//IL_0016: nop
//IL_0017: ret
// Add IL code for set method
g.Emit(OpCodes.Nop);
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Ldarg_1);
g.Emit(OpCodes.Call, pi.GetSetMethod());
// IL_0000: ldarg.0
// IL_0001: call instance class [Esiur]Esiur.Resource.Instance [Esiur]Esiur.Resource.Resource::get_Instance()
// IL_0006: ldstr "Level3"
//IL_000b: callvirt instance void [Esiur]Esiur.Resource.Instance::Modified(string)
//IL_0010: ret
// Call property changed for object
g.Emit(OpCodes.Nop);
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Call, instanceGet);
g.Emit(OpCodes.Dup);
g.Emit(OpCodes.Brtrue_S, callModified);
g.Emit(OpCodes.Pop);
g.Emit(OpCodes.Br_S, exitMethod);
g.MarkLabel(callModified);
g.Emit(OpCodes.Ldstr, pi.Name);
g.Emit(OpCodes.Callvirt, modifyMethod);
g.Emit(OpCodes.Nop);
g.MarkLabel(exitMethod);
g.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(builder);
// create get method
*/
}
}

View File

@@ -0,0 +1,466 @@
using Esiur.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using System.Text.RegularExpressions;
using Esiur.Resource;
using System.Diagnostics;
using Esiur.Data.Types;
using Esiur.Protocol;
namespace Esiur.Proxy;
public static class TypeDefGenerator
{
internal static Regex urlRegex = new Regex(@"^(?:([\S]*)://([^/]*)/?)");
//public static string ToLiteral(string valueTextForCompiler)
//{
// return SymbolDisplay.FormatLiteral(valueTextForCompiler, false);
//}
static string ToLiteral(string input)
{
if (input == null) return "null";
var literal = new StringBuilder();
literal.Append("\"");
foreach (var c in input)
{
switch (c)
{
case '\"': literal.Append("\\\""); break;
case '\\': literal.Append(@"\\"); break;
case '\0': literal.Append(@"\0"); break;
case '\a': literal.Append(@"\a"); break;
case '\b': literal.Append(@"\b"); break;
case '\f': literal.Append(@"\f"); break;
case '\n': literal.Append(@"\n"); break;
case '\r': literal.Append(@"\r"); break;
case '\t': literal.Append(@"\t"); break;
case '\v': literal.Append(@"\v"); break;
default:
// ASCII printable character
if (c >= 0x20 && c <= 0x7e)
{
literal.Append(c);
// As UTF16 escaped character
}
else
{
literal.Append(@"\u");
literal.Append(((int)c).ToString("x4"));
}
break;
}
}
literal.Append("\"");
return literal.ToString();
}
internal static string GenerateRecord(TypeDef typeDef, TypeDef[] typeDefs)
{
var cls = typeDef.Name.Split('.');
var nameSpace = string.Join(".", cls.Take(cls.Length - 1));
var className = cls.Last();
var rt = new StringBuilder();
rt.AppendLine("using System;\r\nusing Esiur.Resource;\r\nusing Esiur.Core;\r\nusing Esiur.Data;\r\nusing Esiur.Protocol;");
rt.AppendLine($"namespace {nameSpace} {{");
if (typeDef.Annotations != null)
{
foreach (var ann in typeDef.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
}
rt.AppendLine($"[TypeId(\"{typeDef.Id.Data.ToHex(0, 16, null)}\")]");
rt.AppendLine($"[Export] public class {className} : IRecord {{");
foreach (var p in typeDef.Properties)
{
var pdTypeName = GetTypeName(p.ValueType, typeDefs);
if (p.Annotations != null)
{
foreach (var ann in p.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
}
rt.AppendLine($"public {pdTypeName} {p.Name} {{ get; set; }}");
rt.AppendLine();
}
rt.AppendLine("\r\n}\r\n}");
return rt.ToString();
}
internal static string GenerateEnum(TypeDef typeDef, TypeDef[] typeDefs)
{
var cls = typeDef.Name.Split('.');
var nameSpace = string.Join(".", cls.Take(cls.Length - 1));
var className = cls.Last();
var rt = new StringBuilder();
rt.AppendLine("using System;\r\nusing Esiur.Resource;\r\nusing Esiur.Core;\r\nusing Esiur.Data;\r\nusing Esiur.Protocol;");
rt.AppendLine($"namespace {nameSpace} {{");
if (typeDef.Annotations != null)
{
foreach (var ann in typeDef.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
}
rt.AppendLine($"[TypeId(\"{typeDef.Id.Data.ToHex(0, 16, null)}\")]");
rt.AppendLine($"[Export] public enum {className} {{");
rt.AppendLine(String.Join(",\r\n", typeDef.Constants.Select(x => $"{x.Name}={x.Value}")));
rt.AppendLine("\r\n}\r\n}");
return rt.ToString();
}
static string GetTypeName(Tru tru, TypeDef[] typeDefs)
{
string name;
if (tru.Identifier == TruIdentifier.TypedResource)// == DataType.Resource)
name = typeDefs.First(x => x.Id == tru.UUID && (x.Kind == TypeDefKind.Resource)).Name;
else if (tru.Identifier == TruIdentifier.TypedRecord)
name = typeDefs.First(x => x.Id == tru.UUID && x.Kind == TypeDefKind.Record).Name;
else if (tru.Identifier == TruIdentifier.Enum)
name = typeDefs.First(x => x.Id == tru.UUID && x.Kind == TypeDefKind.Enum).Name;
else if (tru.Identifier == TruIdentifier.TypedList)
name = GetTypeName(tru.SubTypes[0], typeDefs) + "[]";
else if (tru.Identifier == TruIdentifier.TypedMap)
name = "Map<" + GetTypeName(tru.SubTypes[0], typeDefs)
+ "," + GetTypeName(tru.SubTypes[1], typeDefs)
+ ">";
else if (tru.Identifier == TruIdentifier.Tuple2 ||
tru.Identifier == TruIdentifier.Tuple3 ||
tru.Identifier == TruIdentifier.Tuple4 ||
tru.Identifier == TruIdentifier.Tuple5 ||
tru.Identifier == TruIdentifier.Tuple6 ||
tru.Identifier == TruIdentifier.Tuple7)
name = "(" + String.Join(",", tru.SubTypes.Select(x => GetTypeName(x, typeDefs)))
+ ")";
else
{
name = tru.Identifier switch
{
TruIdentifier.Dynamic => "object",
TruIdentifier.Bool => "bool",
TruIdentifier.Char => "char",
TruIdentifier.DateTime => "DateTime",
TruIdentifier.Decimal => "decimal",
TruIdentifier.Float32 => "float",
TruIdentifier.Float64 => "double",
TruIdentifier.Int16 => "short",
TruIdentifier.Int32 => "int",
TruIdentifier.Int64 => "long",
TruIdentifier.Int8 => "sbyte",
TruIdentifier.String => "string",
TruIdentifier.Map => "Map<object, object>",
TruIdentifier.UInt16 => "ushort",
TruIdentifier.UInt32 => "uint",
TruIdentifier.UInt64 => "ulong",
TruIdentifier.UInt8 => "byte",
TruIdentifier.List => "object[]",
TruIdentifier.Resource => "IResource",
TruIdentifier.Record => "IRecord",
_ => "object"
};
}
return (tru.Nullable) ? name + "?" : name;
}
public static string GetTypes(string url, string dir = null, bool tempDir = false, string username = null, string password = null, bool asyncSetters = false)
{
try
{
if (!urlRegex.IsMatch(url))
throw new Exception("Invalid EP URL");
var path = urlRegex.Split(url);
var con = Warehouse.Default.Get<EpConnection>(path[1] + "://" + path[2],
!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password) ? new { Username = username, Password = password } : null
).Wait(20000);
if (con == null)
throw new Exception("Can't connect to server");
if (string.IsNullOrEmpty(dir))
dir = path[2].Replace(":", "_");
var typeDefs = con.GetLinkDefinitions(path[3]).Wait(60000);
// no longer needed
Warehouse.Default.Remove(con);
var dstDir = new DirectoryInfo(tempDir ? Path.GetTempPath() + Path.DirectorySeparatorChar
+ Misc.Global.GenerateCode(20) + Path.DirectorySeparatorChar + dir : dir);
if (!dstDir.Exists)
dstDir.Create();
else
{
foreach (FileInfo file in dstDir.GetFiles())
file.Delete();
}
// make sources
foreach (var td in typeDefs)
{
if (td.Kind == TypeDefKind.Resource)
{
var source = GenerateClass(td, typeDefs, asyncSetters);
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
}
else if (td.Kind == TypeDefKind.Record)
{
var source = GenerateRecord(td, typeDefs);
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
}
else if (td.Kind == TypeDefKind.Enum)
{
var source = GenerateEnum(td, typeDefs);
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
}
}
// generate info class
var typesFile = @"using System;
namespace Esiur {
public static class Generated {
public static Type[] Resources {get;} = new Type[] { " +
string.Join(",", typeDefs.Where(x => x.Kind == TypeDefKind.Resource).Select(x => $"typeof({x.Name})"))
+ @" };
public static Type[] Records { get; } = new Type[] { " +
string.Join(",", typeDefs.Where(x => x.Kind == TypeDefKind.Record).Select(x => $"typeof({x.Name})"))
+ @" };
public static Type[] Enums { get; } = new Type[] { " +
string.Join(",", typeDefs.Where(x => x.Kind == TypeDefKind.Enum).Select(x => $"typeof({x.Name})"))
+ @" };" +
"\r\n } \r\n}";
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + "Esiur.g.cs", typesFile);
return dstDir.FullName;
}
catch (Exception ex)
{
throw ex;
}
}
internal static string GenerateClass(TypeDef typeDef, TypeDef[] typeDefs, bool asyncSetters)
{
var cls = typeDef.Name.Split('.');
var nameSpace = string.Join(".", cls.Take(cls.Length - 1));
var className = cls.Last();
var rt = new StringBuilder();
rt.AppendLine("using System;\r\nusing Esiur.Resource;\r\nusing Esiur.Core;\r\nusing Esiur.Data;\r\nusing Esiur.Protocol;");
rt.AppendLine("#nullable enable");
rt.AppendLine($"namespace {nameSpace} {{");
if (typeDef.Annotations != null)
{
foreach (var ann in typeDef.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
}
rt.AppendLine($"[TypeId(\"{typeDef.Id.Data.ToHex(0, 16, null)}\")]");
// extends
if (typeDef.ParentId == null)
rt.AppendLine($"public class {className} : EpResource {{");
else
rt.AppendLine($"public class {className} : {typeDefs.First(x => x.Id == typeDef.ParentId && x.Kind == TypeDefKind.Resource).Name} {{");
rt.AppendLine($"public {className}(EpConnection connection, uint instanceId, ulong age, string link) : base(connection, instanceId, age, link) {{}}");
rt.AppendLine($"public {className}() {{}}");
foreach (var f in typeDef.Functions)
{
if (f.Inherited)
continue;
var rtTypeName = GetTypeName(f.ReturnType, typeDefs);
var positionalArgs = f.Arguments.Where((x) => !x.Optional).ToArray();
var optionalArgs = f.Arguments.Where((x) => x.Optional).ToArray();
if (f.Annotations != null)
{
foreach (var kv in f.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(kv.Key)}, {ToLiteral(kv.Value)})]");
}
}
if (f.IsStatic)
{
rt.Append($"[Export] public static AsyncReply<{rtTypeName}> {f.Name}(EpConnection connection");
if (positionalArgs.Length > 0)
rt.Append(", " +
String.Join(", ", positionalArgs.Select((a) => GetTypeName(a.Type, typeDefs) + " " + a.Name)));
if (optionalArgs.Length > 0)
rt.Append(", " +
String.Join(", ", optionalArgs.Select((a) => GetTypeName(a.Type.ToNullable(), typeDefs) + " " + a.Name + " = null")));
}
else
{
rt.Append($"[Export] public AsyncReply<{rtTypeName}> {f.Name}(");
if (positionalArgs.Length > 0)
rt.Append(
String.Join(", ", positionalArgs.Select((a) => GetTypeName(a.Type, typeDefs) + " " + a.Name)));
if (optionalArgs.Length > 0)
{
if (positionalArgs.Length > 0) rt.Append(",");
rt.Append(
String.Join(", ", optionalArgs.Select((a) => GetTypeName(a.Type.ToNullable(), typeDefs) + " " + a.Name + " = null")));
}
}
rt.AppendLine(") {");
rt.AppendLine(
$"var args = new Map<byte, object>(){{{String.Join(", ", positionalArgs.Select((e) => "[" + e.Index + "] = " + e.Name))}}};");
foreach (var a in optionalArgs)
{
rt.AppendLine(
$"if ({a.Name} != null) args[{a.Index}] = {a.Name};");
}
rt.AppendLine($"var rt = new AsyncReply<{rtTypeName}>();");
if (f.IsStatic)
rt.AppendLine($"connection.StaticCall(Guid.Parse(\"{typeDef.Id.ToString()}\"), {f.Index}, args)");
else
rt.AppendLine($"_Invoke({f.Index}, args)");
rt.AppendLine($".Then(x => rt.Trigger(({rtTypeName})x))");
rt.AppendLine($".Error(x => rt.TriggerError(x))");
rt.AppendLine($".Chunk(x => rt.TriggerChunk(x));");
rt.AppendLine("return rt; }");
}
foreach (var p in typeDef.Properties)
{
if (p.Inherited)
continue;
if (p.Annotations != null)
{
foreach (var ann in p.Annotations)
{
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
}
var ptTypeName = GetTypeName(p.ValueType, typeDefs);
rt.AppendLine($"[Export] public {ptTypeName} {p.Name} {{");
rt.AppendLine($"get => ({ptTypeName})properties[{p.Index}];");
if (asyncSetters)
rt.AppendLine($"set => SetResourcePropertyAsync({p.Index}, value);");
else
rt.AppendLine($"set => SetResourceProperty({p.Index}, value);");
rt.AppendLine("}");
}
foreach (var c in typeDef.Constants)
{
if (c.Inherited)
continue;
if (c.Annotations != null)
{
foreach (var ann in c.Annotations)
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
var ctTypeName = GetTypeName(c.ValueType, typeDefs);
rt.AppendLine($"[Export] public const {ctTypeName} {c.Name} = {c.Value};");
}
if (typeDef.Events.Length > 0)
{
rt.AppendLine("protected override void _EmitEventByIndex(byte index, object args) {");
rt.AppendLine("switch (index) {");
var eventsList = new StringBuilder();
foreach (var e in typeDef.Events)
{
var etTypeName = GetTypeName(e.ArgumentType, typeDefs);
rt.AppendLine($"case {e.Index}: {e.Name}?.Invoke(({etTypeName})args); break;");
if (e.Annotations != null)
{
foreach (var ann in e.Annotations)
rt.AppendLine($"[Annotation({ToLiteral(ann.Key)}, {ToLiteral(ann.Value)})]");
}
eventsList.AppendLine($"[Export] public event ResourceEventHandler<{etTypeName}> {e.Name};");
}
rt.AppendLine("}}");
rt.AppendLine(eventsList.ToString());
}
rt.AppendLine("\r\n}\r\n}");
return rt.ToString();
}
}