diff --git a/Tools/Substrate.DotNet.Template/templates/Substrate/.substrate/substrate-config.json b/Tools/Substrate.DotNet.Template/templates/Substrate/.substrate/substrate-config.json index b230e07..ffccf1d 100644 --- a/Tools/Substrate.DotNet.Template/templates/Substrate/.substrate/substrate-config.json +++ b/Tools/Substrate.DotNet.Template/templates/Substrate/.substrate/substrate-config.json @@ -4,9 +4,10 @@ "rest_service": "Substrate.RestService", "rest_client": "Substrate.RestClient" }, - "metadata": { - "websocket": "ws://127.0.0.1:9944" - }, + "metadata": { + "websocket": "ws://127.0.0.1:9944", + "refineMetadata": true + }, "rest_client_settings": { "service_assembly": "Substrate.RestService.dll" } diff --git a/Tools/Substrate.DotNet/Extensions/ReflectedEndpointExtensions.cs b/Tools/Substrate.DotNet/Extensions/ReflectedEndpointExtensions.cs index 6f6258d..26217bf 100644 --- a/Tools/Substrate.DotNet/Extensions/ReflectedEndpointExtensions.cs +++ b/Tools/Substrate.DotNet/Extensions/ReflectedEndpointExtensions.cs @@ -20,6 +20,30 @@ namespace Substrate.DotNet.Extensions /// internal static class ReflectedEndpointExtensions { + /// + /// Ensure we are importing all model items. + /// Not actually required since we use fully qualified items but we want to get rid of that later. + /// + /// + /// + public static void ManageNamespace(CodeNamespace currentNamespace, IReflectedEndpointType defaultReturnType) + { + currentNamespace.Imports.Add(new CodeNamespaceImport(defaultReturnType.Type.Namespace)); + foreach (Type genericArgument in defaultReturnType.Type.GenericTypeArguments) + { + currentNamespace.Imports.Add(new CodeNamespaceImport(genericArgument.Namespace)); + + // Also import namespace from generic sub arguments + if (genericArgument.IsGenericType) + { + foreach (Type genericSubArgument in genericArgument.GenericTypeArguments) + { + currentNamespace.Imports.Add(new CodeNamespaceImport(genericSubArgument.Namespace)); + } + } + } + } + /// /// Converts a reflected endpoint to an interface method code element. /// The generated method is being implemented by an actual client. @@ -73,13 +97,7 @@ internal static CodeTypeMember ToMockupInterfaceMethod(this IReflectedEndpoint e IReflectedEndpointType defaultReturnType = endpoint.GetResponse().GetSuccessReturnType(); if (defaultReturnType != null) { - // Ensure we are importing all model items. - // Not actually required since we use fully qualified items but we want to get rid of that later. - currentNamespace.Imports.Add(new CodeNamespaceImport(defaultReturnType.Type.Namespace)); - foreach (Type genericArgument in defaultReturnType.Type.GenericTypeArguments) - { - currentNamespace.Imports.Add(new CodeNamespaceImport(genericArgument.Namespace)); - } + ManageNamespace(currentNamespace, defaultReturnType); method.Parameters.Add(new CodeParameterDeclarationExpression(defaultReturnType.Type, "value")); } @@ -229,13 +247,7 @@ internal static CodeTypeMember ToMockupClientMethod(this IReflectedEndpoint endp IReflectedEndpointType defaultReturnType = endpoint.GetResponse().GetSuccessReturnType(); if (defaultReturnType != null) { - // Ensure we are importing all model items. - // Not actually required since we use fully qualified items but we want to get rid of that later. - clientNamespace.Imports.Add(new CodeNamespaceImport(defaultReturnType.Type.Namespace)); - foreach (Type genericArgument in defaultReturnType.Type.GenericTypeArguments) - { - clientNamespace.Imports.Add(new CodeNamespaceImport(genericArgument.Namespace)); - } + ManageNamespace(clientNamespace, defaultReturnType); method.Parameters.Add(new CodeParameterDeclarationExpression(defaultReturnType.Type, "value")); } @@ -317,14 +329,7 @@ internal static CodeTypeMember ToUnitTestMethod(this IReflectedEndpoint endpoint IReflectedEndpointType defaultReturnType = endpoint.GetResponse().GetSuccessReturnType(); if (defaultReturnType != null) { - // Ensure we are importing all model items. - // Not actually required since we use fully qualified items but we want to get rid of that later. - clientNamespace.Imports.Add(new CodeNamespaceImport(defaultReturnType.Type.Namespace)); - foreach (Type genericArgument in defaultReturnType.Type.GenericTypeArguments) - { - clientNamespace.Imports.Add(new CodeNamespaceImport(genericArgument.Namespace)); - } - + ManageNamespace(clientNamespace, defaultReturnType); GenerateMockupValueStatement(currentMembers, method, defaultReturnType.Type); } else diff --git a/Tools/Substrate.DotNet/Extensions/ReflectedEndpointResponseExtensions.cs b/Tools/Substrate.DotNet/Extensions/ReflectedEndpointResponseExtensions.cs index 92fe06a..ff69ce6 100644 --- a/Tools/Substrate.DotNet/Extensions/ReflectedEndpointResponseExtensions.cs +++ b/Tools/Substrate.DotNet/Extensions/ReflectedEndpointResponseExtensions.cs @@ -27,14 +27,7 @@ internal static CodeTypeReference ToInterfaceMethodReturnType(this IReflectedEnd return new CodeTypeReference(typeof(void)); } - // Ensure we are importing all model items. - // Not actually required since we use fully qualified items but we want to get rid of that later. - currentNamespace.Imports.Add(new CodeNamespaceImport(defaultReturnType.Type.Namespace)); - foreach (Type genericArgument in defaultReturnType.Type.GenericTypeArguments) - { - currentNamespace.Imports.Add(new CodeNamespaceImport(genericArgument.Namespace)); - } - + ReflectedEndpointExtensions.ManageNamespace(currentNamespace, defaultReturnType); return new CodeTypeReference(typeof(Task<>).MakeGenericType(new[] { defaultReturnType.Type })); } diff --git a/Tools/Substrate.DotNet/Program.cs b/Tools/Substrate.DotNet/Program.cs index d29c4d9..0af3cf2 100644 --- a/Tools/Substrate.DotNet/Program.cs +++ b/Tools/Substrate.DotNet/Program.cs @@ -10,6 +10,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Substrate.DotNet.Service.Generators.Base; namespace Substrate.DotNet { @@ -166,11 +167,22 @@ private static async Task UpgradeOrUpdateSubstrateEnvironmentAsync(bool fe Log.Information("Using Runtime {runtime}", configuration.Metadata.Runtime); - // Service + if (configuration.Metadata.IsMetadataRefined) + { + Log.Information("MetaData refined option is activated"); + SolutionGeneratorBase.RefinedUnnecessaryWrapper(metadata); + } + + // NetApi + Log.Information("Generate {NetApi} classes", configuration.Projects.NetApi); GenerateNetApiClasses(metadata, configuration); + + // Service + Log.Information("Generate {RestService} classes", configuration.Projects.RestService); GenerateRestServiceClasses(metadata, configuration); // Client + Log.Information("Generate {RestClient} classes", configuration.Projects.RestClient); GenerateRestClientClasses(configuration); return true; diff --git a/Tools/Substrate.DotNet/Service/Generators/Base/SolutionGeneratorBase.cs b/Tools/Substrate.DotNet/Service/Generators/Base/SolutionGeneratorBase.cs index c70804d..ff5ff6d 100644 --- a/Tools/Substrate.DotNet/Service/Generators/Base/SolutionGeneratorBase.cs +++ b/Tools/Substrate.DotNet/Service/Generators/Base/SolutionGeneratorBase.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; +using System.Reflection.Metadata.Ecma335; namespace Substrate.DotNet.Service.Generators.Base { @@ -145,6 +147,9 @@ private static Dictionary GetRuntimeIndex(Dictionary nodeTypes) { + var metadataNaming = new MetadataNaming(nodeTypes); + List rewritedName = new(); + Dictionary _countPaths = new(); for (uint id = 0; id < nodeTypes.Keys.Max(); id++) { @@ -196,10 +201,207 @@ protected static void GetGenericStructs(Dictionary nodeTypes) if (generics.Contains(key)) { - type.Path[^1] = type.Path[^1] + "T" + (_countPaths.ContainsKey(key) ? _countPaths[key] : 1); + string suggestedClassName = metadataNaming.WriteClassName(type); + if(suggestedClassName != type.Path[^1] && !rewritedName.Any(x => x == suggestedClassName)) + { + type.Path[^1] = suggestedClassName; + } else + { + type.Path[^1] = type.Path[^1] + "T" + (_countPaths.ContainsKey(key) ? _countPaths[key] : 1); + } + rewritedName.Add(type.Path[^1]); } } } } + + /// + /// Refine current metadata by removing unecessary classes that encapsulate mostly Rust lists + /// C# is more permissive so we don't need to wrap BaseVec<> into a class + /// + public static void RefinedUnnecessaryWrapper(MetaData metadata) + { + Dictionary nodeTypes = metadata.NodeMetadata.Types; + var metadataNaming = new MetadataNaming(nodeTypes); + + bool hasSomethingChanged = false; + do + { + IDictionary wrapperNodes = ExtractWrappers(nodeTypes); + + // Loop over all node types to switch sourceId to new destinationId + hasSomethingChanged = MapSourceToDestination(nodeTypes, wrapperNodes); + + if(hasSomethingChanged) + { + RemoveSourceIds(nodeTypes, metadataNaming, wrapperNodes); + RefineModules(metadata, wrapperNodes); + } + } while (hasSomethingChanged); + } + + /// + /// Remove all unecessary id + /// + /// + /// + /// + private static void RemoveSourceIds(Dictionary nodeTypes, MetadataNaming metadataNaming, IDictionary wrapperNodes) + { + foreach (KeyValuePair node in wrapperNodes) + { + Log.Verbose("\t Replace {sourceType} (id = {sourceKey}) by {destinationType} (id = {destinationKey})", metadataNaming.WriteType(node.Key), node.Key, metadataNaming.WriteType(node.Value), node.Value); + nodeTypes.Remove(node.Key); + } + } + + /// + /// Loop over all modules and replace old occurences + /// + /// + /// + private static void RefineModules(MetaData metadata, IDictionary wrapperNodes) + { + Dictionary modules = metadata.NodeMetadata.Modules; + foreach (KeyValuePair module in modules) + { + PalletStorage storage = module.Value.Storage; + + if (storage == null || storage.Entries == null) + { + continue; + } + + foreach (Entry entry in storage.Entries) + { + if (wrapperNodes.ContainsKey(entry.TypeMap.Item1)) + { + entry.TypeMap = new(wrapperNodes[entry.TypeMap.Item1], entry.TypeMap.Item2); + } + if (entry.TypeMap.Item2 != null && wrapperNodes.ContainsKey(entry.TypeMap.Item2.Key)) + { + entry.TypeMap.Item2.Key = wrapperNodes[entry.TypeMap.Item2.Key]; + } + + if (entry.TypeMap.Item2 != null && wrapperNodes.ContainsKey(entry.TypeMap.Item2.Value)) + { + entry.TypeMap.Item2.Value = wrapperNodes[entry.TypeMap.Item2.Value]; + } + + } + } + } + + /// + /// Check every TypeDef composite which have only on TypeDef Sequence as property field. + /// Target multi generic references (BoundedVec, WeakBoundedVec etc) + /// Return a dictionnary of sourceId, destinationId + /// + /// + /// + private static IDictionary ExtractWrappers(Dictionary nodeTypes) + { + var wrappers = + nodeTypes + .Where(x => x.Value.TypeDef == TypeDefEnum.Composite) + .Select(x => (NodeTypeComposite)x.Value) + .Where(x => x.Path != null) + .GroupBy(x => string.Join('.', x.Path)) + .Where(x => x.Count() > 1) + .SelectMany(x => x) + .Where(x => x.TypeFields != null && x.TypeFields.Length == 1) + .Where(x => nodeTypes[x.TypeFields[0].TypeId].TypeDef == TypeDefEnum.Sequence) + .Select(x => new + { + sourceId = x.Id, + sourceName = x.Path != null ? string.Join(".", x.Path) : string.Empty, + destinationId = nodeTypes[x.TypeFields[0].TypeId].Id + }); + + IDictionary wrapperNodes = wrappers.ToDictionary(x => x.sourceId, x => x.destinationId); + return wrapperNodes; + } + + /// + /// Change all occurences of old Id to Destination Id + /// + /// + /// + /// True if a node has been changed + private static bool MapSourceToDestination(Dictionary nodeTypes, IDictionary wrapperNodes) + { + bool anyUpdate = false; + foreach (KeyValuePair node in nodeTypes) + { + switch (node.Value) + { + case NodeTypeVariant detailVariant when detailVariant.Variants is not null: + foreach (NodeTypeField nodeTypeField in detailVariant.Variants + .Where(x => x.TypeFields != null) + .SelectMany(x => x.TypeFields)) + { + if (wrapperNodes.ContainsKey(nodeTypeField.TypeId)) + { + nodeTypeField.TypeId = wrapperNodes[nodeTypeField.TypeId]; + anyUpdate = true; + } + } + break; + + case NodeTypeCompact detailCompact when wrapperNodes.ContainsKey(detailCompact.TypeId): + detailCompact.TypeId = wrapperNodes[detailCompact.TypeId]; + anyUpdate = true; + break; + + case NodeTypeComposite detailComposite when detailComposite.TypeFields is not null: + foreach (NodeTypeField typeField in detailComposite.TypeFields) + { + if (wrapperNodes.ContainsKey(typeField.TypeId)) + { + typeField.TypeId = wrapperNodes[typeField.TypeId]; + anyUpdate = true; + } + } + break; + + case NodeTypeSequence detailSequence when wrapperNodes.ContainsKey(detailSequence.TypeId): + detailSequence.TypeId = wrapperNodes[detailSequence.TypeId]; + anyUpdate = true; + break; + + case NodeTypeTuple detailTuple when detailTuple.TypeIds != null: + for (int i = 0; i < detailTuple.TypeIds.Length; i++) + { + if (wrapperNodes.ContainsKey(detailTuple.TypeIds[i])) + { + detailTuple.TypeIds[i] = wrapperNodes[detailTuple.TypeIds[i]]; + anyUpdate = true; + } + } + break; + + case NodeTypeArray detailArray when wrapperNodes.ContainsKey(detailArray.TypeId): + detailArray.TypeId = wrapperNodes[detailArray.TypeId]; + anyUpdate = true; + break; + + case NodeTypeBitSequence detailBitSequence: + if(wrapperNodes.ContainsKey(detailBitSequence.TypeIdStore)) + { + detailBitSequence.TypeIdStore = wrapperNodes[detailBitSequence.TypeIdStore]; + } + + if (wrapperNodes.ContainsKey(detailBitSequence.TypeIdOrder)) + { + detailBitSequence.TypeIdOrder = wrapperNodes[detailBitSequence.TypeIdOrder]; + } + + string x = "1"; + break; + } + } + + return anyUpdate; + } } } \ No newline at end of file diff --git a/Tools/Substrate.DotNet/Service/Generators/MetadataNaming.cs b/Tools/Substrate.DotNet/Service/Generators/MetadataNaming.cs new file mode 100644 index 0000000..214ac10 --- /dev/null +++ b/Tools/Substrate.DotNet/Service/Generators/MetadataNaming.cs @@ -0,0 +1,163 @@ +using Substrate.DotNet.Extensions; +using Substrate.NetApi.Model.Meta; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Threading.Tasks; + +namespace Substrate.DotNet.Service.Generators +{ + public class MetadataNaming + { + public const string DefaultTypeSeparator = ""; + private readonly Dictionary _nodeTypes; + + public bool DisplayPropertyName { get; init; } = true; + public bool AggregatePropertyType { get; init; } = false; + + public MetadataNaming(Dictionary nodeTypes) + { + _nodeTypes = nodeTypes; + } + + public string WriteType(uint typeId) + => WriteType(GetPalletType(typeId)); + + public string WriteType(NodeType detailType) + { + + if (detailType is NodeTypeVariant detailVariant) + { + return WriteNodeVariant(detailVariant); + } + else if (detailType is NodeTypeCompact detailCompact) + { + return WriteNodeCompact(detailCompact); + } + else if (detailType is NodeTypePrimitive detailPrimitive) + { + return WriteNodePrimitive(detailPrimitive); + } + else if (detailType is NodeTypeComposite detailComposite) + { + return WriteNodeComposite(detailComposite); + } + else if (detailType is NodeTypeSequence detailSequence) + { + return WriteNodeSequence(detailSequence); + } + else if (detailType is NodeTypeTuple detailTuple) + { + return WriteNodeTuple(detailTuple); + } + else if (detailType is NodeTypeArray detailArray) + { + return WriteNodeArray(detailArray); + } + else + { + throw new NotSupportedException("Type not supported yet..."); // BitSequence ?? + } + } + + public string WriteNodeVariant(NodeTypeVariant nodeType) + { + return nodeType.Path[^1]; + } + + public string WriteNodeCompact(NodeTypeCompact nodeType) + { + return $"{nodeType.TypeDef}{WriteType(nodeType.TypeId)}"; + } + + public string WriteNodePrimitive(NodeTypePrimitive nodeType) + { + return nodeType.Primitive.ToString().ToUpperFirst(); + } + + /// + /// + /// + /// + /// Define if we have to display class details + /// + public string WriteNodeComposite(NodeTypeComposite nodeType, bool expandDetails = false) + { + string display = nodeType.Path[^1]; + if (expandDetails && nodeType.TypeParams != null && nodeType.TypeParams.Length > 0) + { + string fullName = string.Join( + DefaultTypeSeparator, + nodeType.TypeFields.Select(x => $"{(DisplayPropertyName ? x.Name?.ToUpperFirst() : string.Empty)}{WriteType(x.TypeId)}")); + + display += $"{DefaultTypeSeparator}{fullName}"; + } + return display; + } + + public string WriteNodeSequence(NodeTypeSequence nodeType) + { + return $"Vec{WriteType(nodeType.TypeId)}"; + } + + public string WriteNodeTuple(NodeTypeTuple nodeType) + { + return $"Tuple{string.Join(DefaultTypeSeparator, nodeType.TypeIds.Select(WriteType))}"; + } + + public string WriteNodeArray(NodeTypeArray nodeType) + { + return $"{nodeType.Length}{WriteType(nodeType.TypeId)}"; + } + + public NodeType GetPalletType(uint typeId) + { + NodeType nodeType = default; + _nodeTypes.TryGetValue(typeId, out nodeType); + + if (nodeType == null) + { + throw new KeyNotFoundException($"{nameof(nodeType)} is not found in current metadata type"); + } + + return nodeType; + } + + public string WriteClassName(NodeTypeComposite nodeType) + { + string display = nodeType.Path[^1]; + if (nodeType.TypeParams != null && nodeType.TypeParams.Length > 0) + { + var propType = nodeType.TypeFields.Select(x => WriteType(x.TypeId)).ToList(); + + string suffix = propType.Count switch + { + 1 => $"{propType[0]}", + 2 when propType[0] == propType[1] => $"{propType[0]}", + > 2 => ((Func)(() => { + if (propType.All(x => propType[0] == x)) { + return $"{propType[0]}s"; + } + + IEnumerable> groupedName = propType.GroupBy(x => x); + if (groupedName.Count() < propType.Count) + { + return string.Join("_", groupedName.Where(x => x.Count() > 1).Select(x => x.Key)); + } + + // No good solution found + return string.Empty; + }))() + }; + + if (!string.IsNullOrEmpty(suffix)) + { + return $"{display}_{suffix}"; + } + } + return display; + } + } +} diff --git a/Tools/Substrate.DotNet/SubstrateConfiguration.cs b/Tools/Substrate.DotNet/SubstrateConfiguration.cs index 2dbd42f..42556f8 100644 --- a/Tools/Substrate.DotNet/SubstrateConfiguration.cs +++ b/Tools/Substrate.DotNet/SubstrateConfiguration.cs @@ -51,6 +51,9 @@ public class SubstrateConfigurationMetadata [JsonIgnore] public string Runtime { get; set; } + + [JsonProperty("refineMetadata")] + public bool IsMetadataRefined { get; set; } } public class SubstrateConfigurationRestClientSettings