mirror of https://github.com/Xpl0itR/protodec.git
272 lines
8.5 KiB
C#
272 lines
8.5 KiB
C#
|
using System;
|
||
|
using System.CodeDom.Compiler;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Diagnostics.CodeAnalysis;
|
||
|
using System.Linq;
|
||
|
using System.Reflection;
|
||
|
using CommunityToolkit.Diagnostics;
|
||
|
|
||
|
namespace LibProtodec;
|
||
|
|
||
|
public sealed class Protodec
|
||
|
{
|
||
|
public delegate bool LookupFunc(string key, [MaybeNullWhen(false)] out string value);
|
||
|
|
||
|
private readonly Dictionary<string, Protobuf> _protobufs;
|
||
|
private readonly HashSet<string> _currentDescent;
|
||
|
|
||
|
public Protodec()
|
||
|
{
|
||
|
_protobufs = new Dictionary<string, Protobuf>();
|
||
|
_currentDescent = new HashSet<string>();
|
||
|
}
|
||
|
|
||
|
public LookupFunc? CustomTypeLookup { get; init; }
|
||
|
|
||
|
public LookupFunc? CustomNameLookup { get; init; }
|
||
|
|
||
|
public IReadOnlyDictionary<string, Protobuf> Protobufs =>
|
||
|
_protobufs;
|
||
|
|
||
|
public void WriteAllTo(IndentedTextWriter writer)
|
||
|
{
|
||
|
Protobuf.WritePreambleTo(writer);
|
||
|
|
||
|
foreach (Protobuf proto in _protobufs.Values)
|
||
|
{
|
||
|
proto.WriteTo(writer);
|
||
|
writer.WriteLine();
|
||
|
writer.WriteLine();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void ParseMessage(Type type, bool skipEnums = false)
|
||
|
{
|
||
|
Guard.IsTrue(type.IsClass);
|
||
|
|
||
|
ParseMessageInternal(type, skipEnums, null);
|
||
|
_currentDescent.Clear();
|
||
|
}
|
||
|
|
||
|
public void ParseEnum(Type type)
|
||
|
{
|
||
|
Guard.IsTrue(type.IsEnum);
|
||
|
|
||
|
ParseEnumInternal(type, null);
|
||
|
_currentDescent.Clear();
|
||
|
}
|
||
|
|
||
|
private bool IsParsed(Type type, Message? parentMessage, out Dictionary<string, Protobuf> protobufs)
|
||
|
{
|
||
|
protobufs = parentMessage is not null && type.IsNested
|
||
|
? parentMessage.Nested
|
||
|
: _protobufs;
|
||
|
|
||
|
return protobufs.ContainsKey(type.Name)
|
||
|
|| !_currentDescent.Add(type.Name);
|
||
|
}
|
||
|
|
||
|
private void ParseMessageInternal(Type messageClass, bool skipEnums, Message? parentMessage)
|
||
|
{
|
||
|
if (IsParsed(messageClass, parentMessage, out Dictionary<string, Protobuf> protobufs))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Message message = new()
|
||
|
{
|
||
|
Name = TranslateProtobufName(messageClass.Name),
|
||
|
AssemblyName = messageClass.Assembly.FullName,
|
||
|
Namespace = messageClass.Namespace
|
||
|
};
|
||
|
|
||
|
FieldInfo[] idFields = messageClass.GetFields(BindingFlags.Public | BindingFlags.Static);
|
||
|
PropertyInfo[] properties = messageClass.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||
|
|
||
|
for (int pi = 0, fi = 0; pi < properties.Length; pi++, fi++)
|
||
|
{
|
||
|
PropertyInfo property = properties[pi];
|
||
|
|
||
|
if (property.GetMethod is null || property.GetMethod.IsVirtual)
|
||
|
{
|
||
|
fi--;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
Type propertyType = property.PropertyType;
|
||
|
|
||
|
// only OneOf enums are defined nested directly in the message class
|
||
|
if (propertyType.IsEnum && propertyType.DeclaringType?.Name == message.Name)
|
||
|
{
|
||
|
string oneOfName = TranslateOneOfName(property.Name);
|
||
|
int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||
|
.Select(field => (int)field.GetRawConstantValue()!)
|
||
|
.Where(id => id > 0)
|
||
|
.ToArray();
|
||
|
|
||
|
message.OneOfs.Add(oneOfName, oneOfProtoFieldIds);
|
||
|
|
||
|
fi--;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
FieldInfo idField = idFields[fi];
|
||
|
Guard.IsTrue(idField.IsLiteral);
|
||
|
Guard.IsEqualTo(idField.FieldType.Name, nameof(Int32));
|
||
|
|
||
|
int msgFieldId = (int)idField.GetRawConstantValue()!;
|
||
|
bool msgFieldIsOptional = false;
|
||
|
string msgFieldType = ParseFieldType(propertyType, skipEnums, message);
|
||
|
string msgFieldName = TranslateMessageFieldName(property.Name);
|
||
|
|
||
|
// optional protobuf fields will generate an additional "Has" get-only boolean property immediately after the real property
|
||
|
if (properties.Length > pi + 1 && properties[pi + 1].PropertyType.Name == nameof(Boolean) && !properties[pi + 1].CanWrite)
|
||
|
{
|
||
|
msgFieldIsOptional = true;
|
||
|
pi++;
|
||
|
}
|
||
|
|
||
|
message.Fields.Add(msgFieldId, (msgFieldIsOptional, msgFieldType, msgFieldName));
|
||
|
}
|
||
|
|
||
|
protobufs.Add(message.Name, message);
|
||
|
}
|
||
|
|
||
|
private void ParseEnumInternal(Type enumEnum, Message? parentMessage)
|
||
|
{
|
||
|
if (IsParsed(enumEnum, parentMessage, out Dictionary<string, Protobuf> protobufs))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Enum @enum = new()
|
||
|
{
|
||
|
Name = TranslateProtobufName(enumEnum.Name),
|
||
|
AssemblyName = enumEnum.Assembly.FullName,
|
||
|
Namespace = enumEnum.Namespace
|
||
|
};
|
||
|
|
||
|
foreach (FieldInfo field in enumEnum.GetFields(BindingFlags.Public | BindingFlags.Static))
|
||
|
{
|
||
|
int enumFieldId = (int)field.GetRawConstantValue()!;
|
||
|
string enumFieldName = TranslateEnumFieldName(field, @enum.Name);
|
||
|
|
||
|
@enum.Fields.Add(enumFieldId, enumFieldName);
|
||
|
}
|
||
|
|
||
|
protobufs.Add(@enum.Name, @enum);
|
||
|
}
|
||
|
|
||
|
private string ParseFieldType(Type type, bool skipEnums, Message message)
|
||
|
{
|
||
|
switch (type.Name)
|
||
|
{
|
||
|
case "ByteString":
|
||
|
return "bytes";
|
||
|
case nameof(String):
|
||
|
return "string";
|
||
|
case nameof(Boolean):
|
||
|
return "bool";
|
||
|
case nameof(Double):
|
||
|
return "double";
|
||
|
case nameof(UInt32):
|
||
|
return "uint32";
|
||
|
case nameof(UInt64):
|
||
|
return "uint64";
|
||
|
case nameof(Int32):
|
||
|
return "int32";
|
||
|
case nameof(Int64):
|
||
|
return "int64";
|
||
|
case nameof(Single):
|
||
|
return "float";
|
||
|
}
|
||
|
|
||
|
switch (type.GenericTypeArguments.Length)
|
||
|
{
|
||
|
case 1:
|
||
|
string t = ParseFieldType(type.GenericTypeArguments[0], skipEnums, message);
|
||
|
return "repeated " + t;
|
||
|
case 2:
|
||
|
string t1 = ParseFieldType(type.GenericTypeArguments[0], skipEnums, message);
|
||
|
string t2 = ParseFieldType(type.GenericTypeArguments[1], skipEnums, message);
|
||
|
return $"map<{t1}, {t2}>";
|
||
|
}
|
||
|
|
||
|
if (CustomTypeLookup?.Invoke(type.Name, out string? fieldType) == true)
|
||
|
{
|
||
|
return fieldType;
|
||
|
}
|
||
|
|
||
|
if (type.IsEnum)
|
||
|
{
|
||
|
if (skipEnums)
|
||
|
{
|
||
|
return "int32";
|
||
|
}
|
||
|
|
||
|
ParseEnumInternal(type, message);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ParseMessageInternal(type, skipEnums, message);
|
||
|
}
|
||
|
|
||
|
if (!type.IsNested)
|
||
|
{
|
||
|
message.Imports.Add(type.Name);
|
||
|
}
|
||
|
|
||
|
return type.Name;
|
||
|
}
|
||
|
|
||
|
private string TranslateProtobufName(string name) =>
|
||
|
CustomNameLookup?.Invoke(name, out string? translatedName) == true
|
||
|
? translatedName
|
||
|
: name;
|
||
|
|
||
|
private string TranslateOneOfName(string oneOfEnumName) =>
|
||
|
TranslateName(oneOfEnumName, out string translatedName)
|
||
|
? translatedName.TrimEnd("Case")
|
||
|
: oneOfEnumName.TrimEnd("Case")
|
||
|
.ToSnakeCaseLower();
|
||
|
|
||
|
private string TranslateMessageFieldName(string fieldName) =>
|
||
|
TranslateName(fieldName, out string translatedName)
|
||
|
? translatedName
|
||
|
: fieldName.ToSnakeCaseLower();
|
||
|
|
||
|
private string TranslateEnumFieldName(FieldInfo field, string enumName)
|
||
|
{
|
||
|
if (field.GetCustomAttributesData()
|
||
|
.SingleOrDefault(attr => attr.AttributeType.Name == "OriginalNameAttribute")
|
||
|
?.ConstructorArguments[0]
|
||
|
.Value
|
||
|
is string originalName)
|
||
|
{
|
||
|
return originalName;
|
||
|
}
|
||
|
|
||
|
if (TranslateName(field.Name, out string translatedName))
|
||
|
{
|
||
|
return translatedName;
|
||
|
}
|
||
|
|
||
|
if (!enumName.IsBeebyted())
|
||
|
{
|
||
|
enumName = enumName.ToSnakeCaseUpper();
|
||
|
}
|
||
|
|
||
|
return enumName + '_' + field.Name.ToSnakeCaseUpper();
|
||
|
}
|
||
|
|
||
|
private bool TranslateName(string name, out string translatedName)
|
||
|
{
|
||
|
if (CustomNameLookup?.Invoke(name, out translatedName!) == true)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
translatedName = name;
|
||
|
return name.IsBeebyted();
|
||
|
}
|
||
|
}
|