Added prototype for converting message classes into example JSON
This commit is contained in:
parent
22e6da5a57
commit
503507422d
133
ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs
Normal file
133
ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ParseTapetiMessagesPrototype
|
||||
{
|
||||
public static class AssemblyLoaderMessageParser
|
||||
{
|
||||
public static void ParseAssembly(string classLibraryFilename)
|
||||
{
|
||||
var loadContext = new AssemblyLoadContext(null, true);
|
||||
try
|
||||
{
|
||||
var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename);
|
||||
|
||||
foreach (var assemblyType in assembly.GetTypes())
|
||||
HandleType(assemblyType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadContext.Unload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void HandleType(Type type)
|
||||
{
|
||||
if (!type.IsClass)
|
||||
return;
|
||||
|
||||
// For this prototype, filter out anything not ending in Message
|
||||
// Might want to show a full tree in PettingZoo since this is just a convention
|
||||
if (!type.Name.EndsWith("Message") || type.Name != "RelatieUpdateMessage")
|
||||
return;
|
||||
|
||||
Console.WriteLine($"{type.Namespace}.{type.Name}");
|
||||
|
||||
// We can't create an instance of the type to serialize easily, as most will depend on
|
||||
// assemblies not included in the NuGet package, so we'll parse the Type ourselves.
|
||||
// This is still slightly easier than using MetadataReader, as we can more easily check against
|
||||
// standard types like Nullable.
|
||||
//
|
||||
// The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models
|
||||
// themselves should not inherit from classes outside of their assembly, or include properties
|
||||
// with types from other assemblies. With that assumption, walking the class structure should be safe.
|
||||
var serialized = TypeToJObject(type);
|
||||
|
||||
Console.WriteLine(serialized);
|
||||
Console.WriteLine("");
|
||||
}
|
||||
|
||||
|
||||
private static JObject TypeToJObject(Type type)
|
||||
{
|
||||
var result = new JObject();
|
||||
|
||||
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
// Note: unfortunately we can not call GetCustomAttributes here, as that would
|
||||
// trigger assemblies not included in the package to be loaded
|
||||
|
||||
var value = PropertyToJToken(propertyInfo.PropertyType);
|
||||
result.Add(propertyInfo.Name, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static readonly Dictionary<Type, JToken> TypeMap = new()
|
||||
{
|
||||
{ typeof(short), 0 },
|
||||
{ typeof(ushort), 0 },
|
||||
{ typeof(int), 0 },
|
||||
{ typeof(uint), 0 },
|
||||
{ typeof(long), 0 },
|
||||
{ typeof(ulong), 0 },
|
||||
{ typeof(decimal), 0.0 },
|
||||
{ typeof(float), 0.0 },
|
||||
{ typeof(bool), false }
|
||||
};
|
||||
|
||||
|
||||
private static JToken PropertyToJToken(Type propertyType)
|
||||
{
|
||||
var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
|
||||
|
||||
// String is also a class
|
||||
if (actualType == typeof(string))
|
||||
return "";
|
||||
|
||||
|
||||
if (actualType.IsClass)
|
||||
{
|
||||
// IEnumerable<T>
|
||||
var enumerableInterface = actualType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
|
||||
if (enumerableInterface != null)
|
||||
return new JArray(TypeToJObject(enumerableInterface.GetGenericArguments()[0]));
|
||||
|
||||
|
||||
return TypeToJObject(actualType);
|
||||
}
|
||||
|
||||
if (actualType.IsArray)
|
||||
return new JArray(TypeToJObject(actualType.GetElementType()));
|
||||
|
||||
if (actualType.IsEnum)
|
||||
return Enum.GetNames(actualType).FirstOrDefault();
|
||||
|
||||
|
||||
// Special cases for runtime generated values
|
||||
if (actualType == typeof(DateTime))
|
||||
{
|
||||
// Strip the milliseconds for a cleaner result
|
||||
var now = DateTime.UtcNow;
|
||||
return new DateTime(now.Ticks - now.Ticks % TimeSpan.TicksPerSecond, now.Kind);
|
||||
}
|
||||
|
||||
if (actualType == typeof(Guid))
|
||||
return Guid.NewGuid().ToString();
|
||||
|
||||
return TypeMap.TryGetValue(actualType, out var mappedToken)
|
||||
? mappedToken
|
||||
: $"(unknown type: {actualType.Name})";
|
||||
}
|
||||
}
|
||||
}
|
164
ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs
Normal file
164
ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs
Normal file
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ParseTapetiMessagesPrototype
|
||||
{
|
||||
public static class MetadataReaderMessageParser
|
||||
{
|
||||
public static void ParseAssembly(string classLibraryFilename)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fileStream = new FileStream(classLibraryFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var peReader = new PEReader(fileStream);
|
||||
|
||||
var metadataReader = peReader.GetMetadataReader();
|
||||
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var typeDefinitionHandle in metadataReader.TypeDefinitions)
|
||||
{
|
||||
var typeDefinition = metadataReader.GetTypeDefinition(typeDefinitionHandle);
|
||||
HandleTypeDefinition(metadataReader, typeDefinition);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void HandleTypeDefinition(MetadataReader metadataReader, TypeDefinition typeDefinition)
|
||||
{
|
||||
var typeNamespace = metadataReader.GetString(typeDefinition.Namespace);
|
||||
var typeName = metadataReader.GetString(typeDefinition.Name);
|
||||
|
||||
// For this prototype, filter out anything not ending in Message
|
||||
// Might want to show a full tree in PettingZoo since this is just a convention
|
||||
if (!typeName.EndsWith("Message"))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"{typeNamespace}.{typeName}");
|
||||
|
||||
var example = new JObject();
|
||||
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var propertyDefinitionHandle in typeDefinition.GetProperties())
|
||||
{
|
||||
// TODO get properties from base class
|
||||
|
||||
var propertyDefinition = metadataReader.GetPropertyDefinition(propertyDefinitionHandle);
|
||||
HandlePropertyDefinition(metadataReader, propertyDefinition, example);
|
||||
}
|
||||
|
||||
Console.WriteLine(example.ToString());
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
private static void HandlePropertyDefinition(MetadataReader metadataReader, PropertyDefinition propertyDefinition, JObject targetObject)
|
||||
{
|
||||
var fieldName = metadataReader.GetString(propertyDefinition.Name);
|
||||
var signature = propertyDefinition.DecodeSignature(new JsonSignatureProvider(), null);
|
||||
|
||||
targetObject.Add(fieldName, signature.ReturnType);
|
||||
}
|
||||
|
||||
|
||||
private class JsonSignatureProvider : ISignatureTypeProvider<JToken, object>
|
||||
{
|
||||
public JToken GetPrimitiveType(PrimitiveTypeCode typeCode)
|
||||
{
|
||||
return typeCode switch
|
||||
{
|
||||
PrimitiveTypeCode.Boolean => false,
|
||||
|
||||
PrimitiveTypeCode.Byte or
|
||||
PrimitiveTypeCode.Int16 or
|
||||
PrimitiveTypeCode.Int32 or
|
||||
PrimitiveTypeCode.Int64 or
|
||||
PrimitiveTypeCode.IntPtr or
|
||||
PrimitiveTypeCode.SByte or
|
||||
PrimitiveTypeCode.UInt16 or
|
||||
PrimitiveTypeCode.UInt32 or
|
||||
PrimitiveTypeCode.UInt64 or
|
||||
PrimitiveTypeCode.UIntPtr => 0,
|
||||
|
||||
PrimitiveTypeCode.Char or
|
||||
PrimitiveTypeCode.String => "",
|
||||
|
||||
PrimitiveTypeCode.Double or
|
||||
PrimitiveTypeCode.Single => 0.0,
|
||||
|
||||
// TODO recurse
|
||||
PrimitiveTypeCode.Object => "OBJECT",
|
||||
|
||||
_ => $"Unsupported primitive type code: {typeCode}"
|
||||
};
|
||||
}
|
||||
|
||||
public JToken GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind = 0) => "typedef";
|
||||
|
||||
public JToken GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind = 0)
|
||||
{
|
||||
var typeReference = reader.GetTypeReference(handle);
|
||||
var typeName = reader.GetString(typeReference.Name);
|
||||
|
||||
return typeName;
|
||||
}
|
||||
|
||||
|
||||
public JToken GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind = 0) => "typespec";
|
||||
|
||||
public JToken GetSZArrayType(JToken elementType) => new JValue(elementType + "[]");
|
||||
public JToken GetPointerType(JToken elementType) => null;
|
||||
public JToken GetByReferenceType(JToken elementType) => null;
|
||||
public JToken GetGenericMethodParameter(object genericContext, int index) => "!!" + index;
|
||||
public JToken GetGenericTypeParameter(object genericContext, int index) => "!" + index;
|
||||
|
||||
public JToken GetPinnedType(JToken elementType) => elementType + " pinned";
|
||||
public JToken GetGenericInstantiation(JToken genericType, ImmutableArray<JToken> typeArguments) => genericType + "<" + string.Join(",", typeArguments) + ">";
|
||||
public JToken GetModifiedType(JToken modifierType, JToken unmodifiedType, bool isRequired) => unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifierType + ")";
|
||||
|
||||
public JToken GetArrayType(JToken elementType, ArrayShape shape)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append(elementType);
|
||||
builder.Append('[');
|
||||
|
||||
for (int i = 0; i < shape.Rank; i++)
|
||||
{
|
||||
int lowerBound = 0;
|
||||
|
||||
if (i < shape.LowerBounds.Length)
|
||||
{
|
||||
lowerBound = shape.LowerBounds[i];
|
||||
builder.Append(lowerBound);
|
||||
}
|
||||
|
||||
builder.Append("...");
|
||||
|
||||
if (i < shape.Sizes.Length)
|
||||
{
|
||||
builder.Append(lowerBound + shape.Sizes[i] - 1);
|
||||
}
|
||||
|
||||
if (i < shape.Rank - 1)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public JToken GetFunctionPointerType(MethodSignature<JToken> signature) => "methodptr(something)";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
21
ParseTapetiMessagesPrototype/Program.cs
Normal file
21
ParseTapetiMessagesPrototype/Program.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace ParseTapetiMessagesPrototype
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
const string classLibraryFilename = "D:\\Temp\\lib\\netstandard2.0\\Messaging.Relatie.dll";
|
||||
|
||||
// There are advantages to using the MetadataReader, for example no code is run (LoadAssemblyForReflection is no longer
|
||||
// supported in .NET Core) and the assembly is not locked at all. This comes at the cost of complexity however, so
|
||||
// this prototype explores both options.
|
||||
//
|
||||
// In the final version perhaps we can work around loading the assembly into our own process by spawning a new process
|
||||
// to convert it into metadata used by the main process.
|
||||
|
||||
//MetadataReaderMessageParser.ParseAssembly(classLibraryFilename);
|
||||
|
||||
AssemblyLoaderMessageParser.ParseAssembly(classLibraryFilename);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,9 +10,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseTapetiMessagesPrototype", "ParseTapetiMessagesPrototype\ParseTapetiMessagesPrototype.csproj", "{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -32,6 +34,10 @@ Global
|
||||
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
Loading…
Reference in New Issue
Block a user