From 31bda58460601d754bc3aad8cb3eb0899ae40e98 Mon Sep 17 00:00:00 2001 From: ahmed Date: Mon, 13 Jul 2026 08:39:52 +0300 Subject: [PATCH] IndexedStructure --- Libraries/Esiur/Data/Codec.cs | 55 +++- Libraries/Esiur/Data/DataDeserializer.cs | 50 ++++ Libraries/Esiur/Data/DataSerializer.cs | 46 ++- Libraries/Esiur/Data/IndexAttribute.cs | 29 ++ Libraries/Esiur/Data/IndexedStructure.cs | 21 ++ Libraries/Esiur/Data/IndexedStructureCodec.cs | 276 ++++++++++++++++++ Libraries/Esiur/Data/ParsedTdu.cs | 6 +- Libraries/Esiur/Data/PlainTdu.cs | 2 +- Libraries/Esiur/Data/Tdu.cs | 11 +- Libraries/Esiur/Data/TduIdentifier.cs | 9 +- Libraries/Esiur/Data/Types/ArgumentDefInfo.cs | 15 + Libraries/Esiur/Data/Types/ConstantDefInfo.cs | 12 + Libraries/Esiur/Data/Types/EventDef.cs | 39 +++ Libraries/Esiur/Data/Types/EventDefInfo.cs | 19 ++ Libraries/Esiur/Data/Types/FunctionDef.cs | 143 +++++---- Libraries/Esiur/Data/Types/FunctionDefInfo.cs | 16 + Libraries/Esiur/Data/Types/MemberDefInfo.cs | 71 +++++ Libraries/Esiur/Data/Types/PropertyDef.cs | 56 ++++ Libraries/Esiur/Data/Types/PropertyDefInfo.cs | 19 ++ Libraries/Esiur/Data/Types/RemoteTypeDef.cs | 154 ++++++++++ Libraries/Esiur/Data/Types/TypeDef.cs | 5 +- Libraries/Esiur/Data/Types/TypeDefInfo.cs | 168 +++++++++++ .../Resource/Attributes/StorageAttribute.cs | 15 - .../Esiur/Resource/PropertyPermission.cs | 12 +- Tests/Unit/IndexedStructureTests.cs | 89 ++++++ Tests/Unit/TypeDefInfoSerializationTests.cs | 130 +++++++++ 26 files changed, 1381 insertions(+), 87 deletions(-) create mode 100644 Libraries/Esiur/Data/IndexAttribute.cs create mode 100644 Libraries/Esiur/Data/IndexedStructure.cs create mode 100644 Libraries/Esiur/Data/IndexedStructureCodec.cs create mode 100644 Libraries/Esiur/Data/Types/ArgumentDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/ConstantDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/EventDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/FunctionDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/MemberDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/PropertyDefInfo.cs create mode 100644 Libraries/Esiur/Data/Types/TypeDefInfo.cs delete mode 100644 Libraries/Esiur/Resource/Attributes/StorageAttribute.cs create mode 100644 Tests/Unit/IndexedStructureTests.cs create mode 100644 Tests/Unit/TypeDefInfoSerializationTests.cs diff --git a/Libraries/Esiur/Data/Codec.cs b/Libraries/Esiur/Data/Codec.cs index 0b3bc77..b030e09 100644 --- a/Libraries/Esiur/Data/Codec.cs +++ b/Libraries/Esiur/Data/Codec.cs @@ -102,6 +102,8 @@ public static class Codec static AsyncParser[] TypedAsyncParsers = new AsyncParser[] { DataDeserializer.TypedParserAsync, + DataDeserializer.TypeDefInfoParserAsync, + DataDeserializer.TruParserAsync, }; static AsyncParser[] ExtendedAsyncParsers = new AsyncParser[] @@ -168,6 +170,8 @@ public static class Codec static SyncParser[] TypedParsers = new SyncParser[] { DataDeserializer.TypedParser, + DataDeserializer.TypeDefInfoParser, + DataDeserializer.TruParser, //DataDeserializer.RecordParser, //DataDeserializer.TypedListParser, //DataDeserializer.TypedMapParser, @@ -541,7 +545,15 @@ public static class Codec type = valueOrSource.GetType(); - if (Composers.ContainsKey(type)) + if (valueOrSource is Tru) + { + return DataSerializer.TruComposer(valueOrSource, warehouse, connection); + } + else if (valueOrSource is Types.TypeDefInfo) + { + return DataSerializer.TypeDefComposer(valueOrSource, warehouse, connection); + } + else if (Composers.ContainsKey(type)) { return Composers[type](valueOrSource, warehouse, connection); } @@ -555,6 +567,10 @@ public static class Codec { return DataSerializer.RecordComposer(valueOrSource, warehouse, connection); } + else if (valueOrSource is IndexedStructure) + { + return DataSerializer.StructureComposer(valueOrSource, warehouse, connection); + } else if (type.IsGenericType) { var genericType = type.GetGenericTypeDefinition(); @@ -636,6 +652,43 @@ public static class Codec return tdu.Composed; } + /// + /// Encodes a local indexed structure. This is equivalent to , but + /// makes the expected structure type explicit at call sites such as TypeDef and authentication. + /// + public static byte[] ComposeIndexedType(T value, Warehouse warehouse, EpConnection connection) + where T : IndexedStructure + => Compose(value, warehouse, connection); + + /// + /// Parses one indexed structure and returns both the consumed byte count and typed value. + /// Unknown indexes are ignored and missing indexes retain their CLR defaults. + /// + public static (uint Size, T Value) ParseIndexedType(byte[] data, uint offset, Warehouse warehouse) + where T : IndexedStructure + { + var (size, value) = ParseSync(data, offset, warehouse); + return (size, value is T typed ? typed : IndexedStructureCodec.FromMap(value)); + } + + /// + /// Parses an already framed TDU as an indexed structure. + /// + public static T ParseIndexedType(PlainTdu tdu, Warehouse warehouse) + where T : IndexedStructure + { + var value = ParseSync(tdu, warehouse); + return value is T typed ? typed : IndexedStructureCodec.FromMap(value); + } + + /// + /// Converts an indexed map already decoded by the codec to a typed structure without + /// reparsing any wire data. + /// + public static T ParseIndexedType(object indexedMap) + where T : IndexedStructure + => indexedMap is T typed ? typed : IndexedStructureCodec.FromMap(indexedMap); + public static bool IsAnonymous(Type type) { // Detect anonymous types diff --git a/Libraries/Esiur/Data/DataDeserializer.cs b/Libraries/Esiur/Data/DataDeserializer.cs index 9e93c2d..01ecb73 100644 --- a/Libraries/Esiur/Data/DataDeserializer.cs +++ b/Libraries/Esiur/Data/DataDeserializer.cs @@ -11,14 +11,57 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Runtime.InteropServices.ComTypes; using System.Text; +using System.Threading; namespace Esiur.Data; public static class DataDeserializer { + internal static readonly AsyncLocal TypeDefRequestSequence = new(); + + public static async AsyncReply TypeDefInfoParserAsync( + ParsedTdu tdu, EpConnection connection, uint[] requestSequence) + { + var parsed = await Codec.ParseAsync(tdu.Data, tdu.PayloadOffset, connection, requestSequence); + if (parsed.Size != tdu.PayloadLength) + throw new InvalidDataException("The TypeDef payload contains trailing or incomplete data."); + + return IndexedStructureCodec.FromMap(parsed.Value); + } + + public static object TypeDefInfoParser(ParsedTdu tdu, Warehouse warehouse) + { + var (size, value) = Codec.ParseSync(tdu.Data, tdu.PayloadOffset, warehouse); + if (size != tdu.PayloadLength) + throw new InvalidDataException("The TypeDef payload contains trailing or incomplete data."); + + return IndexedStructureCodec.FromMap(value); + } + + public static async AsyncReply TruParserAsync( + ParsedTdu tdu, EpConnection connection, uint[] requestSequence) + { + var parsed = await Tru.ParseAsync(tdu.Data, tdu.PayloadOffset, connection, + TypeDefRequestSequence.Value); + if (parsed.Size != tdu.PayloadLength) + throw new InvalidDataException("The TRU payload contains trailing or incomplete data."); + + return parsed.Value; + } + + public static object TruParser(ParsedTdu tdu, Warehouse warehouse) + { + var parsed = Tru.Parse(tdu.Data, tdu.PayloadOffset, warehouse); + if (parsed.Size != tdu.PayloadLength) + throw new InvalidDataException("The TRU payload contains trailing or incomplete data."); + + return parsed.Value; + } + public static object NullParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence) { return null; @@ -445,6 +488,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } else if (current.Identifier == TduIdentifier.TypeOfTarget) @@ -596,6 +640,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } else if (current.Identifier == TduIdentifier.TypeOfTarget) @@ -908,6 +953,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } @@ -959,6 +1005,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } @@ -1163,6 +1210,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } else if (current.Identifier == TduIdentifier.TypeOfTarget) @@ -1271,6 +1319,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } } @@ -1624,6 +1673,7 @@ public static class DataDeserializer { current.Class = previous.Value.Class; current.Identifier = previous.Value.Identifier; + current.Index = previous.Value.Index; current.Metadata = previous.Value.Metadata; } else if (current.Identifier == TduIdentifier.TypeOfTarget) diff --git a/Libraries/Esiur/Data/DataSerializer.cs b/Libraries/Esiur/Data/DataSerializer.cs index 8a160c1..7fbecb1 100644 --- a/Libraries/Esiur/Data/DataSerializer.cs +++ b/Libraries/Esiur/Data/DataSerializer.cs @@ -567,9 +567,13 @@ public static class DataSerializer { var tdu = Codec.ComposeInternal(i, warehouse, connection); - var currentTru = Tru.FromType(i?.GetType(), warehouse); + // Structures are deliberately schema-less and therefore have no TRU of their + // own; their composed value is a typed byte/dynamic map. + var currentTru = IndexedStructureCodec.ContainsStructure(i?.GetType()) + ? null + : Tru.FromType(i?.GetType(), warehouse); - if (tdu.Class == TduClass.Typed && tru.Match(currentTru)) + if (tdu.Class == TduClass.Typed && currentTru != null && tru.Match(currentTru)) { var d = tdu.Composed.Clip(tdu.ContentOffset, (uint)tdu.Composed.Length - tdu.ContentOffset); @@ -605,6 +609,12 @@ public static class DataSerializer { var elementTru = Tru.FromType(type, warehouse); + // Local structures and other schema-less CLR types intentionally have no TRU. + // Preserve them as a dynamic list; the indexed binder reconstructs the requested + // collection type after parsing. + if (elementTru == null) + return ListComposer(value, warehouse, connection); + byte[] composed = TypedArrayComposer(value, elementTru, warehouse, connection); if (composed == null) @@ -859,6 +869,38 @@ public static class DataSerializer return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null); } + /// + /// Composes an indexed CLR structure using the compatible Map<byte, object> wire shape. + /// + public static Tdu StructureComposer(object value, Warehouse warehouse, EpConnection connection) + { + if (value == null) + return new Tdu(TduIdentifier.Null, Array.Empty(), 0, null, null); + + var map = IndexedStructureCodec.ToMap((IndexedStructure)value); + return TypedMapComposer(map, typeof(byte), typeof(object), warehouse, connection); + } + + /// + /// Carries a TRU as a self-framed value, allowing it to appear in dynamic structure fields. + /// + public static Tdu TruComposer(object value, Warehouse warehouse, EpConnection connection) + { + var data = ((Tru)value).Compose(connection); + return new Tdu(TduIdentifier.TRU, data, (ulong)data.Length, null, connection); + } + + /// + /// Carries a TypeDefInfo in the dedicated TypeDef slot. Its payload remains an indexed + /// structure so new fields can be added without changing this outer framing. + /// + public static Tdu TypeDefComposer(object value, Warehouse warehouse, EpConnection connection) + { + var structure = StructureComposer(value, warehouse, connection); + return new Tdu(TduIdentifier.TypeDef, structure.Composed, + (ulong)structure.Composed.Length, null, connection); + } + public static unsafe Tdu UUIDComposer(object value, Warehouse warehouse, EpConnection connection) { return new Tdu(TduIdentifier.UUID, ((Uuid)value).Data, 16, null, null); diff --git a/Libraries/Esiur/Data/IndexAttribute.cs b/Libraries/Esiur/Data/IndexAttribute.cs new file mode 100644 index 0000000..c72666d --- /dev/null +++ b/Libraries/Esiur/Data/IndexAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace Esiur.Data; + +/// +/// Assigns a stable wire index to a field or property of a . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = false, Inherited = true)] +public sealed class IndexAttribute : Attribute +{ + /// + /// Gets or sets the byte-sized index used on the wire. + /// + public byte Index { get; set; } + + public IndexAttribute() + { + } + + public IndexAttribute(int index) + { + if (index < byte.MinValue || index > byte.MaxValue) + throw new ArgumentOutOfRangeException(nameof(index), + "A structure index must be between 0 and 255."); + + Index = (byte)index; + } +} diff --git a/Libraries/Esiur/Data/IndexedStructure.cs b/Libraries/Esiur/Data/IndexedStructure.cs new file mode 100644 index 0000000..1d3ee2a --- /dev/null +++ b/Libraries/Esiur/Data/IndexedStructure.cs @@ -0,0 +1,21 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Esiur.Data; + +/// +/// Base class for local, schema-less types whose members are identified by byte indexes. +/// Structures use the same wire representation as Map<byte, object>, but can be +/// composed and parsed directly as CLR types through . +/// +public abstract class IndexedStructure +{ + +} + diff --git a/Libraries/Esiur/Data/IndexedStructureCodec.cs b/Libraries/Esiur/Data/IndexedStructureCodec.cs new file mode 100644 index 0000000..a7871c6 --- /dev/null +++ b/Libraries/Esiur/Data/IndexedStructureCodec.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Esiur.Data +{ + internal static class IndexedStructureCodec + { + private sealed class IndexedMember + { + public byte Index { get; } + public string Name { get; } + public Type Type { get; } + public Func Get { get; } + public Action Set { get; } + + public IndexedMember(byte index, PropertyInfo property) + { + Index = index; + Name = property.Name; + Type = property.PropertyType; + Get = property.GetValue; + Set = property.SetValue; + } + + public IndexedMember(byte index, FieldInfo field) + { + Index = index; + Name = field.Name; + Type = field.FieldType; + Get = field.GetValue; + Set = field.SetValue; + } + } + + private static readonly ConcurrentDictionary Members = new(); + + internal static Map ToMap(IndexedStructure value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + var map = new Map(); + foreach (var member in GetMembers(value.GetType())) + { + var memberValue = member.Get(value); + if (memberValue is not null) + map[member.Index] = PrepareForComposition(memberValue); + } + + return map; + } + + internal static T FromMap(object value) where T : IndexedStructure + => (T)FromMap(value, typeof(T)); + + internal static object FromMap(object value, Type targetType) + { + if (!typeof(IndexedStructure).IsAssignableFrom(targetType)) + throw new ArgumentException($"{targetType.FullName} does not inherit {nameof(IndexedStructure)}.", + nameof(targetType)); + + if (value is not IDictionary map) + throw new InvalidDataException( + $"Cannot parse {targetType.FullName}: expected an indexed map but received {value?.GetType().FullName ?? "null"}."); + + object instance; + try + { + instance = Activator.CreateInstance(targetType, true)!; + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Structure type {targetType.FullName} must have a parameterless constructor.", ex); + } + + var members = GetMembers(targetType).ToDictionary(x => x.Index); + foreach (DictionaryEntry entry in map) + { + byte index; + try + { + index = Convert.ToByte(entry.Key); + } + catch (Exception ex) + { + throw new InvalidDataException("A structure field index is not a byte value.", ex); + } + + // Sparse structures are version tolerant: fields unknown to this CLR type are ignored. + if (!members.TryGetValue(index, out var member)) + continue; + + try + { + member.Set(instance, ConvertValue(entry.Value, member.Type)); + } + catch (Exception ex) when (ex is not InvalidDataException) + { + throw new InvalidDataException( + $"Could not assign structure field {targetType.Name}.{member.Name} (index {index}).", ex); + } + } + + return instance; + } + + private static IndexedMember[] GetMembers(Type type) + => Members.GetOrAdd(type, DiscoverMembers); + + private static IndexedMember[] DiscoverMembers(Type type) + { + if (!typeof(IndexedStructure).IsAssignableFrom(type)) + throw new InvalidOperationException($"{type.FullName} does not inherit {nameof(IndexedStructure)}."); + + var members = new List(); + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; + + foreach (var property in type.GetProperties(flags)) + { + var attribute = property.GetCustomAttribute(true); + if (attribute is null) + continue; + + if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) + throw new InvalidOperationException( + $"Indexed property {type.FullName}.{property.Name} must be a readable, writable, non-indexer property."); + + members.Add(new IndexedMember(attribute.Index, property)); + } + + foreach (var field in type.GetFields(flags)) + { + var attribute = field.GetCustomAttribute(true); + if (attribute is null) + continue; + + if (field.IsInitOnly || field.IsLiteral) + throw new InvalidOperationException( + $"Indexed field {type.FullName}.{field.Name} must be writable."); + + members.Add(new IndexedMember(attribute.Index, field)); + } + + var duplicate = members.GroupBy(x => x.Index).FirstOrDefault(x => x.Count() > 1); + if (duplicate is not null) + throw new InvalidOperationException( + $"Structure type {type.FullName} uses index {duplicate.Key} more than once."); + + return members.OrderBy(x => x.Index).ToArray(); + } + + private static object? PrepareForComposition(object? value) + { + if (value is IndexedStructure structure) + return ToMap(structure); + + // Protocol-owned enums are values, not distributed enum TypeDefs. Carry their + // underlying integer so an indexed structure has no warehouse-registration dependency. + if (value is Enum enumValue) + return Convert.ChangeType(enumValue, Enum.GetUnderlyingType(enumValue.GetType())); + + // A collection containing structures cannot advertise the local CLR structure type + // through TRU. Encode it as a dynamic list and recursively turn its items into maps. + if (value is IEnumerable sequence && value is not string && value is not byte[] && value is not IDictionary) + { + var items = sequence.Cast().ToArray(); + if (items.Any(x => x is IndexedStructure || ContainsStructure(x?.GetType()))) + return items.Select(PrepareForComposition).ToArray(); + } + + return value; + } + + internal static bool ContainsStructure(Type? type) + { + if (type is null) + return false; + if (typeof(IndexedStructure).IsAssignableFrom(type)) + return true; + if (type.IsArray) + return ContainsStructure(type.GetElementType()); + if (type.IsGenericType) + return type.GetGenericArguments().Any(ContainsStructure); + return false; + } + + private static object? ConvertValue(object? value, Type targetType) + { + if (value is null) + { + if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) is null) + return Activator.CreateInstance(targetType); + return null; + } + + if (targetType.IsInstanceOfType(value)) + return value; + + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (typeof(IndexedStructure).IsAssignableFrom(underlyingType)) + return FromMap(value, underlyingType); + + if (targetType.IsArray && value is IEnumerable arraySource) + { + var elementType = targetType.GetElementType()!; + var values = arraySource.Cast().Select(x => ConvertValue(x, elementType)).ToArray(); + var array = Array.CreateInstance(elementType, values.Length); + for (var i = 0; i < values.Length; i++) + array.SetValue(values[i], i); + return array; + } + + if (TryGetListElementType(targetType, out var listElementType) && value is IEnumerable listSource) + { + var concreteType = targetType.IsInterface || targetType.IsAbstract + ? typeof(List<>).MakeGenericType(listElementType) + : targetType; + var list = (IList)Activator.CreateInstance(concreteType)!; + foreach (var item in listSource) + list.Add(ConvertValue(item, listElementType)); + return list; + } + + if (TryGetMapTypes(targetType, out var keyType, out var valueType) && value is IDictionary sourceMap) + { + var concreteType = targetType.IsInterface || targetType.IsAbstract + ? typeof(Dictionary<,>).MakeGenericType(keyType, valueType) + : targetType; + var targetMap = (IDictionary)Activator.CreateInstance(concreteType)!; + foreach (DictionaryEntry item in sourceMap) + targetMap.Add(ConvertValue(item.Key, keyType), ConvertValue(item.Value, valueType)); + return targetMap; + } + + return RuntimeCaster.Cast(value, targetType); + } + + private static bool TryGetListElementType(Type type, out Type elementType) + { + var candidate = type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(List<>) || + type.GetGenericTypeDefinition() == typeof(IList<>) || + type.GetGenericTypeDefinition() == typeof(ICollection<>) || + type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + ? type + : type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IList<>)); + + elementType = candidate?.GetGenericArguments()[0] ?? typeof(object); + return candidate is not null; + } + + private static bool TryGetMapTypes(Type type, out Type keyType, out Type valueType) + { + var candidate = type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Map<,>) || + type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + ? type + : type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + + var arguments = candidate?.GetGenericArguments(); + keyType = arguments?[0] ?? typeof(object); + valueType = arguments?[1] ?? typeof(object); + return candidate is not null; + } + } + +} diff --git a/Libraries/Esiur/Data/ParsedTdu.cs b/Libraries/Esiur/Data/ParsedTdu.cs index 101bbba..26986bb 100644 --- a/Libraries/Esiur/Data/ParsedTdu.cs +++ b/Libraries/Esiur/Data/ParsedTdu.cs @@ -75,7 +75,7 @@ namespace Esiur.Data Ends = ends }; } - else if (cls == TduClass.Typed) + else if ((h & 0xC7) == 0x80) // cls == TduClass = Typed & Identifier = Typed { ulong cll = (ulong)(h >> 3) & 0x7; @@ -206,7 +206,7 @@ namespace Esiur.Data Ends = ends }; } - else if (cls == TduClass.Typed) + else if ((h & 0xC7) == 0x80) // else if (cls == TduClass.Typed) { ulong cll = (ulong)(h >> 3) & 0x7; @@ -395,7 +395,7 @@ namespace Esiur.Data Ends = ends }; } - else if (cls == TduClass.Typed) + else if ((h & 0xC7) == 0x80) // (cls == TduClass.Typed) { ulong cll = (ulong)(h >> 3) & 0x7; diff --git a/Libraries/Esiur/Data/PlainTdu.cs b/Libraries/Esiur/Data/PlainTdu.cs index 05cc3e2..3667e21 100644 --- a/Libraries/Esiur/Data/PlainTdu.cs +++ b/Libraries/Esiur/Data/PlainTdu.cs @@ -75,7 +75,7 @@ namespace Esiur.Data Ends = ends }; } - else if (cls == TduClass.Typed) + else if ((h & 0xC7) == 0x80) // else if (cls == TduClass.Typed) { ulong cll = (ulong)(h >> 3) & 0x7; diff --git a/Libraries/Esiur/Data/Tdu.cs b/Libraries/Esiur/Data/Tdu.cs index 4418482..5908526 100644 --- a/Libraries/Esiur/Data/Tdu.cs +++ b/Libraries/Esiur/Data/Tdu.cs @@ -81,7 +81,8 @@ public struct Tdu Composed = DC.Combine(new byte[] { (byte)Identifier }, 0, 1, data, 0, (uint)length); } else if (Class == TduClass.Dynamic - || Class == TduClass.Extension) + || Class == TduClass.Extension + || (Class == TduClass.Typed && Identifier != TduIdentifier.Typed)) { if (length == 0) @@ -287,6 +288,14 @@ public struct Tdu if (Class != TduClass.Typed || with.Class != TduClass.Typed) return false; + // Dedicated typed-class values (TypeDef/TRU) carry no metadata. Their identifier + // completely describes the outer type and is sufficient for continuation encoding. + if (Identifier != TduIdentifier.Typed) + return true; + + if (Metadata == null || with.Metadata == null) + return false; + if (!Metadata.Match(with.Metadata)) return false; diff --git a/Libraries/Esiur/Data/TduIdentifier.cs b/Libraries/Esiur/Data/TduIdentifier.cs index 9a608a1..d387358 100644 --- a/Libraries/Esiur/Data/TduIdentifier.cs +++ b/Libraries/Esiur/Data/TduIdentifier.cs @@ -51,16 +51,11 @@ namespace Esiur.Data MapList = 0x47, Typed = 0x80, + TypeDef = 0x81, + TRU = 0x82, - //Record = 0x80, - //TypedList = 0x81, - //TypedMap = 0x82, - //TypedTuple = 0x83, - //TypedEnum = 0x84, - //TypedConstant = 0x85, TypeContinuation = 0xC0, TypeOfTarget = 0xC1, - TypeDef = 0xC2, } } diff --git a/Libraries/Esiur/Data/Types/ArgumentDefInfo.cs b/Libraries/Esiur/Data/Types/ArgumentDefInfo.cs new file mode 100644 index 0000000..e073388 --- /dev/null +++ b/Libraries/Esiur/Data/Types/ArgumentDefInfo.cs @@ -0,0 +1,15 @@ +#nullable enable + +namespace Esiur.Data.Types; + +/// +/// Indexed function-argument definition carried inside . +/// +public class ArgumentDefInfo : MemberDefInfo +{ + [Index((int)ArgumentDefField.ValueType)] + public Tru ValueType { get; set; } = null!; + + [Index((int)ArgumentDefField.DefaultValue)] + public object? DefaultValue { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/ConstantDefInfo.cs b/Libraries/Esiur/Data/Types/ConstantDefInfo.cs new file mode 100644 index 0000000..ef2cfdc --- /dev/null +++ b/Libraries/Esiur/Data/Types/ConstantDefInfo.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace Esiur.Data.Types; + +public class ConstantDefInfo : MemberDefInfo +{ + [Index((int)ConstantDefField.ValueType)] + public Tru ValueType { get; set; } = null!; + + [Index((int)ConstantDefField.Value)] + public object? Value { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/EventDef.cs b/Libraries/Esiur/Data/Types/EventDef.cs index a6ec2c5..2710e72 100644 --- a/Libraries/Esiur/Data/Types/EventDef.cs +++ b/Libraries/Esiur/Data/Types/EventDef.cs @@ -28,12 +28,51 @@ public class EventDef : MemberDef public bool AutoDelivered { get; set; } + // Compatibility for existing subscription paths while callers migrate to AutoDelivered. + public bool Subscribable + { + get => !AutoDelivered; + set => AutoDelivered = !value; + } + public bool Deprecated { get; set; } public EventInfo EventInfo { get; set; } public Tru ArgumentType { get; set; } + public static async AsyncReply> ParseAsync( + byte[] data, uint offset, byte index, bool inherited, + EpConnection connection, ulong[] requestSequence) + { + var originalOffset = offset; + var hasAnnotations = (data[offset] & 0x10) != 0; + var subscribable = (data[offset++] & 0x08) != 0; + var name = data.GetString(offset + 1, data[offset]); + offset += (uint)data[offset] + 1; + + var argumentType = await Tru.ParseAsync(data, offset, connection, requestSequence); + offset += argumentType.Size; + + Map annotations = null; + if (hasAnnotations) + { + var (size, value) = Codec.ParseSync(data, offset, null); + annotations = value as Map; + offset += size; + } + + return new ParseResult(new EventDef + { + Index = index, + Name = name, + Inherited = inherited, + ArgumentType = argumentType.Value, + Subscribable = subscribable, + Annotations = annotations, + }, offset - originalOffset); + } + //public static async AsyncReply> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence) diff --git a/Libraries/Esiur/Data/Types/EventDefInfo.cs b/Libraries/Esiur/Data/Types/EventDefInfo.cs new file mode 100644 index 0000000..c4974b8 --- /dev/null +++ b/Libraries/Esiur/Data/Types/EventDefInfo.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Esiur.Data.Types; + +public class EventDefInfo : MemberDefInfo +{ + [Index((int)EventDefField.ArgumentType)] + public Tru ArgumentType { get; set; } = null!; + + [Index((int)EventDefField.ArgumentName)] + public string? ArgumentName { get; set; } + + [Index((int)EventDefField.OrderingControl)] + public OrderingControl OrderingControl { get; set; } + + // Kept byte-sized until the protocol defines a HistoryControl enum. + [Index((int)EventDefField.HistoryControl)] + public byte HistoryControl { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/FunctionDef.cs b/Libraries/Esiur/Data/Types/FunctionDef.cs index ae2015d..cdeda32 100644 --- a/Libraries/Esiur/Data/Types/FunctionDef.cs +++ b/Libraries/Esiur/Data/Types/FunctionDef.cs @@ -46,49 +46,38 @@ public class FunctionDef : MemberDef set; } - - public static async AsyncReply> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence) + public static async AsyncReply> ParseAsync( + byte[] data, uint offset, byte index, bool inherited, + EpConnection connection, ulong[] requestSequence) { - - var oOffset = offset; - - var isStatic = ((data[offset] & 0x4) == 0x4); - var hasAnnotation = ((data[offset++] & 0x10) == 0x10); - + var originalOffset = offset; + var isStatic = (data[offset] & 0x04) != 0; + var hasAnnotations = (data[offset++] & 0x10) != 0; var name = data.GetString(offset + 1, data[offset]); offset += (uint)data[offset] + 1; - //Console.WriteLine("Parsing functionDef " + name); - - // return type var returnType = await Tru.ParseAsync(data, offset, connection, requestSequence); offset += returnType.Size; - - // arguments count - var argsCount = data[offset++]; - List arguments = new(); - for (var a = 0; a < argsCount; a++) + var argumentCount = data[offset++]; + var arguments = new List(argumentCount); + for (var argumentIndex = 0; argumentIndex < argumentCount; argumentIndex++) { - var argType = await ArgumentDef.ParseAsync(data, offset, a, connection, requestSequence); - arguments.Add(argType.Value); - offset += argType.Size; + var argument = await ArgumentDef.ParseAsync( + data, offset, argumentIndex, connection, requestSequence); + arguments.Add(argument.Value); + offset += argument.Size; } Map annotations = null; - - // arguments - if (hasAnnotation) // Annotation ? + if (hasAnnotations) { - var (len, anns) = Codec.ParseSync(data, offset, null); - - if (anns is Map map) - annotations = map; - - offset += len; + var (size, value) = Codec.ParseSync(data, offset, null); + annotations = value as Map; + offset += size; } - return new ParseResult( new FunctionDef() + return new ParseResult(new FunctionDef { Index = index, Name = name, @@ -97,35 +86,89 @@ public class FunctionDef : MemberDef Inherited = inherited, Annotations = annotations, ReturnType = returnType.Value, - }, offset - oOffset); + }, offset - originalOffset); } - public byte[] Compose(EpConnection connection) - { - var name = DC.ToBytes(Name); + //public static async AsyncReply> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence) + //{ - var bl = new BinaryList() - .AddUInt8((byte)name.Length) - .AddUInt8Array(name) - .AddUInt8Array(ReturnType.Compose(connection)) - .AddUInt8((byte)Arguments.Length); + // var oOffset = offset; - for (var i = 0; i < Arguments.Length; i++) - bl.AddUInt8Array(Arguments[i].Compose(connection)); + // var isStatic = ((data[offset] & 0x4) == 0x4); + // var hasAnnotation = ((data[offset++] & 0x10) == 0x10); + + // var name = data.GetString(offset + 1, data[offset]); + // offset += (uint)data[offset] + 1; + + // //Console.WriteLine("Parsing functionDef " + name); + + // // return type + // var returnType = await Tru.ParseAsync(data, offset, connection, requestSequence); + // offset += returnType.Size; + + // // arguments count + // var argsCount = data[offset++]; + // List arguments = new(); + + // for (var a = 0; a < argsCount; a++) + // { + // var argType = await ArgumentDef.ParseAsync(data, offset, a, connection, requestSequence); + // arguments.Add(argType.Value); + // offset += argType.Size; + // } + + // Map annotations = null; + + // // arguments + // if (hasAnnotation) // Annotation ? + // { + // var (len, anns) = Codec.ParseSync(data, offset, null); + + // if (anns is Map map) + // annotations = map; + + // offset += len; + // } + + // return new ParseResult( new FunctionDef() + // { + // Index = index, + // Name = name, + // Arguments = arguments.ToArray(), + // IsStatic = isStatic, + // Inherited = inherited, + // Annotations = annotations, + // ReturnType = returnType.Value, + // }, offset - oOffset); + //} + + //public byte[] Compose(EpConnection connection) + //{ + + // var name = DC.ToBytes(Name); + + // var bl = new BinaryList() + // .AddUInt8((byte)name.Length) + // .AddUInt8Array(name) + // .AddUInt8Array(ReturnType.Compose(connection)) + // .AddUInt8((byte)Arguments.Length); + + // for (var i = 0; i < Arguments.Length; i++) + // bl.AddUInt8Array(Arguments[i].Compose(connection)); - if (Annotations != null) - { - var exp = Codec.Compose(Annotations, connection.Instance.Warehouse , connection);// DC.ToBytes(Annotation); - bl.AddUInt8Array(exp); - bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0))); - } - else - bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x80 : (byte)0x0) | (IsStatic ? 0x4 : 0))); + // if (Annotations != null) + // { + // var exp = Codec.Compose(Annotations, connection.Instance.Warehouse , connection);// DC.ToBytes(Annotation); + // bl.AddUInt8Array(exp); + // bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0))); + // } + // else + // bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x80 : (byte)0x0) | (IsStatic ? 0x4 : 0))); - return bl.ToArray(); - } + // return bl.ToArray(); + //} public static FunctionDef MakeFunctionDef(Warehouse warehouse, Type type, MethodInfo mi, byte index, string name, TypeDef schema) diff --git a/Libraries/Esiur/Data/Types/FunctionDefInfo.cs b/Libraries/Esiur/Data/Types/FunctionDefInfo.cs new file mode 100644 index 0000000..4e429c6 --- /dev/null +++ b/Libraries/Esiur/Data/Types/FunctionDefInfo.cs @@ -0,0 +1,16 @@ +#nullable enable +using System.Collections.Generic; + +namespace Esiur.Data.Types; + +public class FunctionDefInfo : MemberDefInfo +{ + [Index((int)FunctionDefField.Arguments)] + public List? Arguments { get; set; } + + [Index((int)FunctionDefField.ReturnType)] + public Tru ReturnType { get; set; } = null!; + + [Index((int)FunctionDefField.StreamMode)] + public StreamMode StreamMode { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/MemberDefInfo.cs b/Libraries/Esiur/Data/Types/MemberDefInfo.cs new file mode 100644 index 0000000..e075640 --- /dev/null +++ b/Libraries/Esiur/Data/Types/MemberDefInfo.cs @@ -0,0 +1,71 @@ +#nullable enable +using System.Collections.Generic; + +namespace Esiur.Data.Types; + +/// +/// Wire representation shared by all TypeDef members. +/// +public class MemberDefInfo : IndexedStructure +{ + [Index((int)MemberDefField.Index)] + public byte Index { get; set; } + + [Index((int)MemberDefField.Name)] + public string Name { get; set; } = string.Empty; + + /// + /// Raw member-specific flags. Interpret this as PropertyDefFlags, FunctionDefFlags, + /// EventDefFlags, ConstantDefFlags, or ArgumentDefFlags according to the concrete type. + /// + [Index((int)MemberDefField.Flags)] + public byte Flags { get; set; } + + [Index((int)MemberDefField.Description)] + public string? Description { get; set; } + + [Index((int)MemberDefField.Usage)] + public string? Usage { get; set; } + + [Index((int)MemberDefField.Examples)] + public List? Examples { get; set; } + + [Index((int)MemberDefField.Tags)] + public List? Tags { get; set; } + + [Index((int)MemberDefField.Unit)] + public string? Unit { get; set; } + + [Index((int)MemberDefField.Minimum)] + public object? Minimum { get; set; } + + [Index((int)MemberDefField.Maximum)] + public object? Maximum { get; set; } + + [Index((int)MemberDefField.AllowedValues)] + public List? AllowedValues { get; set; } + + [Index((int)MemberDefField.Pattern)] + public string? Pattern { get; set; } + + [Index((int)MemberDefField.Format)] + public string? Format { get; set; } + + [Index((int)MemberDefField.Preconditions)] + public List? Preconditions { get; set; } + + [Index((int)MemberDefField.Postconditions)] + public List? Postconditions { get; set; } + + [Index((int)MemberDefField.Effects)] + public OperationEffects Effects { get; set; } + + [Index((int)MemberDefField.Warnings)] + public List? Warnings { get; set; } + + [Index((int)MemberDefField.RelatedMembers)] + public List? RelatedMembers { get; set; } + + [Index((int)MemberDefField.DeprecationMessage)] + public string? DeprecationMessage { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/PropertyDef.cs b/Libraries/Esiur/Data/Types/PropertyDef.cs index 930277a..feabe22 100644 --- a/Libraries/Esiur/Data/Types/PropertyDef.cs +++ b/Libraries/Esiur/Data/Types/PropertyDef.cs @@ -44,11 +44,58 @@ public class PropertyDef : MemberDef public bool ReadOnly { get; set; } + // Compatibility for code still consuming the previous permission model. + public PropertyPermission Permission + { + get => ReadOnly ? PropertyPermission.Read : PropertyPermission.ReadWrite; + set => ReadOnly = value == PropertyPermission.Read; + } + public bool Constant { get; set; } //public bool IsNullable { get; set; } public bool Historical { get; set; } + public bool HasHistory + { + get => Historical; + set => Historical = value; + } + + public static async AsyncReply> ParseAsync( + byte[] data, uint offset, byte index, bool inherited, + EpConnection connection, ulong[] requestSequence) + { + var originalOffset = offset; + var hasAnnotations = (data[offset] & 0x08) != 0; + var hasHistory = (data[offset] & 0x01) != 0; + var permission = (PropertyPermission)((data[offset++] >> 1) & 0x03); + var name = data.GetString(offset + 1, data[offset]); + offset += (uint)data[offset] + 1; + + var valueType = await Tru.ParseAsync(data, offset, connection, requestSequence); + offset += valueType.Size; + + Map annotations = null; + if (hasAnnotations) + { + var (size, value) = Codec.ParseSync(data, offset, null); + annotations = value as Map; + offset += size; + } + + return new ParseResult(new PropertyDef + { + Index = index, + Name = name, + Inherited = inherited, + Permission = permission, + HasHistory = hasHistory, + ValueType = valueType.Value, + Annotations = annotations, + }, offset - originalOffset); + } + /* public PropertyType Mode { @@ -273,6 +320,15 @@ public class PropertyDef : MemberDef } + public static PropertyDef MakePropertyDef( + Warehouse warehouse, Type type, PropertyInfo pi, string name, byte index, + PropertyPermission permission, TypeDef typeDef) + { + var definition = MakePropertyDef(warehouse, type, pi, name, index, typeDef); + definition.Permission = permission; + return definition; + } + public static string GetTypeAnnotationName(Type type) { diff --git a/Libraries/Esiur/Data/Types/PropertyDefInfo.cs b/Libraries/Esiur/Data/Types/PropertyDefInfo.cs new file mode 100644 index 0000000..8413730 --- /dev/null +++ b/Libraries/Esiur/Data/Types/PropertyDefInfo.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Esiur.Data.Types; + +public class PropertyDefInfo : MemberDefInfo +{ + [Index((int)PropertyDefField.ValueType)] + public Tru ValueType { get; set; } = null!; + + [Index((int)PropertyDefField.OrderingControl)] + public OrderingControl OrderingControl { get; set; } + + // Kept byte-sized until the protocol defines a HistoryControl enum. + [Index((int)PropertyDefField.HistoryControl)] + public byte HistoryControl { get; set; } + + [Index((int)PropertyDefField.DefaultValue)] + public object? DefaultValue { get; set; } +} diff --git a/Libraries/Esiur/Data/Types/RemoteTypeDef.cs b/Libraries/Esiur/Data/Types/RemoteTypeDef.cs index 2e7d34d..6e6fc5a 100644 --- a/Libraries/Esiur/Data/Types/RemoteTypeDef.cs +++ b/Libraries/Esiur/Data/Types/RemoteTypeDef.cs @@ -44,6 +44,26 @@ public class RemoteTypeDef:TypeDef { uint ends = offset + contentLength; + if (((byte)data[offset] & 0xC7) == (byte)TduIdentifier.TypeDef) + { + var previousSequence = DataDeserializer.TypeDefRequestSequence.Value; + try + { + DataDeserializer.TypeDefRequestSequence.Value = requestSequence; + var parsed = await Codec.ParseAsync(data, offset, connection, null); + if (parsed.Size != contentLength || parsed.Value is not TypeDefInfo info) + throw new Exception("Invalid TypeDefInfo payload."); + + ApplyInfo(od, domain, data, offset, contentLength, info); + CompleteRegistration(od, connection); + return od; + } + finally + { + DataDeserializer.TypeDefRequestSequence.Value = previousSequence; + } + } + uint oOffset = offset; // start parsing... @@ -161,5 +181,139 @@ public class RemoteTypeDef:TypeDef return od; } + + private static void ApplyInfo(RemoteTypeDef definition, string domain, byte[] data, + uint offset, uint contentLength, TypeDefInfo info) + { + definition._domain = domain; + definition._content = data.Clip(offset, contentLength); + definition._typeId = info.Id; + definition._parentTypeId = info.Parent; + definition._typeName = string.IsNullOrEmpty(info.Namespace) + ? info.Name + : $"{info.Namespace}.{info.Name}"; + definition._typeDefKind = info.Kind; + definition._version = info.Version; + definition.Annotations = info.Annotations; + + definition._properties.Clear(); + definition._functions.Clear(); + definition._events.Clear(); + definition._constants.Clear(); + + if (info.Properties != null) + definition._properties.AddRange(info.Properties.Select(x => ToProperty(definition, x))); + if (info.Functions != null) + definition._functions.AddRange(info.Functions.Select(x => ToFunction(definition, x))); + if (info.Events != null) + definition._events.AddRange(info.Events.Select(x => ToEvent(definition, x))); + if (info.Constants != null) + definition._constants.AddRange(info.Constants.Select(x => ToConstant(definition, x))); + } + + private static T ApplyMember(RemoteTypeDef definition, MemberDefInfo info, T member) + where T : MemberDef + { + member.Definition = definition; + member.Index = info.Index; + member.Name = info.Name; + member.Inherited = (info.Flags & (byte)MemberDefFlags.Inherited) != 0; + member.Description = info.Description; + member.Usage = info.Usage; + member.Examples = info.Examples; + member.Tags = info.Tags; + member.Unit = info.Unit; + member.Minimum = info.Minimum; + member.Maximum = info.Maximum; + member.AllowedValues = info.AllowedValues; + member.Pattern = info.Pattern; + member.Format = info.Format; + member.Preconditions = info.Preconditions; + member.Postconditions = info.Postconditions; + member.Effects = info.Effects; + member.Warnings = info.Warnings; + member.RelatedMembers = info.RelatedMembers; + member.DeprecationMessage = info.DeprecationMessage; + return member; + } + + private static PropertyDef ToProperty(RemoteTypeDef definition, PropertyDefInfo info) + { + var flags = (PropertyDefFlags)info.Flags; + return ApplyMember(definition, info, new PropertyDef + { + ValueType = info.ValueType, + ReadOnly = flags.HasFlag(PropertyDefFlags.ReadOnly), + Constant = flags.HasFlag(PropertyDefFlags.Constant), + Historical = flags.HasFlag(PropertyDefFlags.Historical) || info.HistoryControl != 0, + }); + } + + private static FunctionDef ToFunction(RemoteTypeDef definition, FunctionDefInfo info) + { + var flags = (FunctionDefFlags)info.Flags; + return ApplyMember(definition, info, new FunctionDef + { + ReturnType = info.ReturnType, + StreamMode = info.StreamMode, + IsStatic = flags.HasFlag(FunctionDefFlags.Static), + ReadOnly = flags.HasFlag(FunctionDefFlags.ReadOnly), + Idempotent = flags.HasFlag(FunctionDefFlags.Idempotent), + Cancellable = flags.HasFlag(FunctionDefFlags.Cancellable), + Deprecated = flags.HasFlag(FunctionDefFlags.Deprecated), + Arguments = info.Arguments?.Select(ToArgument).ToArray() ?? Array.Empty(), + }); + } + + private static ArgumentDef ToArgument(ArgumentDefInfo info) + { + var flags = (ArgumentDefFlags)info.Flags; + return new ArgumentDef + { + Index = info.Index, + Name = info.Name, + Optional = flags.HasFlag(ArgumentDefFlags.Optional), + Type = info.ValueType, + DefaultValue = info.DefaultValue, + }; + } + + private static EventDef ToEvent(RemoteTypeDef definition, EventDefInfo info) + { + var flags = (EventDefFlags)info.Flags; + return ApplyMember(definition, info, new EventDef + { + ArgumentType = info.ArgumentType, + AutoDelivered = flags.HasFlag(EventDefFlags.AutoDelivered), + Deprecated = flags.HasFlag(EventDefFlags.Deprecated), + }); + } + + private static ConstantDef ToConstant(RemoteTypeDef definition, ConstantDefInfo info) + => ApplyMember(definition, info, new ConstantDef + { + ValueType = info.ValueType, + Value = info.Value, + }); + + private static void CompleteRegistration(RemoteTypeDef definition, EpConnection connection) + { + definition._proxyType = connection.Instance?.Warehouse?.TryGetProxyType( + definition.Kind, definition.Domain, definition.Name); + + if (definition._proxyType != null) + { + foreach (var property in definition.Properties) + property.PropertyInfo = definition._proxyType.GetProperty(property.Name); + foreach (var function in definition.Functions) + function.MethodInfo = definition._proxyType.GetMethod(function.Name); + foreach (var eventDefinition in definition.Events) + eventDefinition.EventInfo = definition._proxyType.GetEvent(eventDefinition.Name); + foreach (var constant in definition.Constants) + constant.FieldInfo = definition._proxyType.GetField(constant.Name); + } + + connection.Instance?.Warehouse?.TryRegisterRemoteTypeDef(connection.RemoteDomain, definition); + } } diff --git a/Libraries/Esiur/Data/Types/TypeDef.cs b/Libraries/Esiur/Data/Types/TypeDef.cs index fe54274..6e5b75c 100644 --- a/Libraries/Esiur/Data/Types/TypeDef.cs +++ b/Libraries/Esiur/Data/Types/TypeDef.cs @@ -46,6 +46,8 @@ public class TypeDef public TypeDefKind Kind => _typeDefKind; + public int Version => _version; + public EventDef GetEventDefByName(string eventName) { foreach (var i in _events) @@ -151,7 +153,8 @@ public class TypeDef public virtual byte[] Compose(EpConnection connection) { - return null; + var warehouse = connection?.Instance?.Warehouse ?? Warehouse.Default; + return Codec.Compose(TypeDefInfo.FromTypeDef(this), warehouse, connection); } diff --git a/Libraries/Esiur/Data/Types/TypeDefInfo.cs b/Libraries/Esiur/Data/Types/TypeDefInfo.cs new file mode 100644 index 0000000..8a82bc2 --- /dev/null +++ b/Libraries/Esiur/Data/Types/TypeDefInfo.cs @@ -0,0 +1,168 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; + +namespace Esiur.Data.Types; + +/// +/// Indexed, transport-safe representation of a type definition. +/// +public class TypeDefInfo : IndexedStructure +{ + [Index((int)TypeDefField.Version)] + public int Version { get; set; } + + [Index((int)TypeDefField.Id)] + public ulong Id { get; set; } + + [Index((int)TypeDefField.Name)] + public string Name { get; set; } = string.Empty; + + [Index((int)TypeDefField.Namespace)] + public string? Namespace { get; set; } + + [Index((int)TypeDefField.Kind)] + public TypeDefKind Kind { get; set; } + + [Index((int)TypeDefField.Parent)] + public ulong? Parent { get; set; } + + [Index((int)TypeDefField.Properties)] + public List? Properties { get; set; } + + [Index((int)TypeDefField.Functions)] + public List? Functions { get; set; } + + [Index((int)TypeDefField.Events)] + public List? Events { get; set; } + + [Index((int)TypeDefField.Constants)] + public List? Constants { get; set; } + + [Index((int)TypeDefField.Usage)] + public string? Usage { get; set; } + + [Index((int)TypeDefField.Description)] + public string? Description { get; set; } + + [Index((int)TypeDefField.Example)] + public object? Example { get; set; } + + [Index((int)TypeDefField.Category)] + public string? Category { get; set; } + + [Index((int)TypeDefField.Since)] + public string? Since { get; set; } + + [Index((int)TypeDefField.Annotations)] + public Map? Annotations { get; set; } + + public static TypeDefInfo FromTypeDef(TypeDef definition) + { + return new TypeDefInfo + { + Version = definition.Version, + Id = definition.Id, + Name = definition.Name, + Kind = definition.Kind, + Parent = definition.ParentTypeId, + Annotations = definition.Annotations, + Properties = definition.Properties.Select(FromProperty).ToList(), + Functions = definition.Functions.Select(FromFunction).ToList(), + Events = definition.Events.Select(FromEvent).ToList(), + Constants = definition.Constants.Select(FromConstant).ToList(), + }; + } + + private static T CopyMember(MemberDef source, T target) where T : MemberDefInfo + { + target.Index = source.Index; + target.Name = source.Name; + target.Description = source.Description; + target.Usage = source.Usage; + target.Examples = source.Examples; + target.Tags = source.Tags; + target.Unit = source.Unit; + target.Minimum = source.Minimum; + target.Maximum = source.Maximum; + target.AllowedValues = source.AllowedValues; + target.Pattern = source.Pattern; + target.Format = source.Format; + target.Preconditions = source.Preconditions; + target.Postconditions = source.Postconditions; + target.Effects = source.Effects; + target.Warnings = source.Warnings; + target.RelatedMembers = source.RelatedMembers; + target.DeprecationMessage = source.DeprecationMessage; + return target; + } + + private static PropertyDefInfo FromProperty(PropertyDef source) + { + var flags = source.Inherited ? PropertyDefFlags.Inherited : PropertyDefFlags.None; + if (source.ReadOnly) flags |= PropertyDefFlags.ReadOnly; + if (source.Constant) flags |= PropertyDefFlags.Constant; + if (source.Historical) flags |= PropertyDefFlags.Historical; + + return CopyMember(source, new PropertyDefInfo + { + Flags = (byte)flags, + ValueType = source.ValueType, + HistoryControl = source.Historical ? (byte)1 : (byte)0, + }); + } + + private static FunctionDefInfo FromFunction(FunctionDef source) + { + var flags = source.Inherited ? FunctionDefFlags.Inherited : FunctionDefFlags.None; + if (source.Deprecated) flags |= FunctionDefFlags.Deprecated; + if (source.IsStatic) flags |= FunctionDefFlags.Static; + if (source.ReadOnly) flags |= FunctionDefFlags.ReadOnly; + if (source.Idempotent) flags |= FunctionDefFlags.Idempotent; + if (source.Cancellable) flags |= FunctionDefFlags.Cancellable; + + return CopyMember(source, new FunctionDefInfo + { + Flags = (byte)flags, + ReturnType = source.ReturnType, + StreamMode = source.StreamMode, + Arguments = source.Arguments?.Select(FromArgument).ToList(), + }); + } + + private static ArgumentDefInfo FromArgument(ArgumentDef source) + { + return new ArgumentDefInfo + { + Index = checked((byte)source.Index), + Name = source.Name, + Flags = source.Optional ? (byte)ArgumentDefFlags.Optional : (byte)ArgumentDefFlags.None, + ValueType = source.Type, + DefaultValue = source.DefaultValue, + }; + } + + private static EventDefInfo FromEvent(EventDef source) + { + var flags = source.Inherited ? EventDefFlags.Inherited : EventDefFlags.None; + if (source.Deprecated) flags |= EventDefFlags.Deprecated; + if (source.AutoDelivered) flags |= EventDefFlags.AutoDelivered; + + return CopyMember(source, new EventDefInfo + { + Flags = (byte)flags, + ArgumentType = source.ArgumentType, + }); + } + + private static ConstantDefInfo FromConstant(ConstantDef source) + { + var flags = source.Inherited ? ConstantDefFlags.Inherited : ConstantDefFlags.None; + return CopyMember(source, new ConstantDefInfo + { + Flags = (byte)flags, + ValueType = source.ValueType, + Value = source.Value, + }); + } +} diff --git a/Libraries/Esiur/Resource/Attributes/StorageAttribute.cs b/Libraries/Esiur/Resource/Attributes/StorageAttribute.cs deleted file mode 100644 index e608f27..0000000 --- a/Libraries/Esiur/Resource/Attributes/StorageAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Esiur.Resource; - -[AttributeUsage(AttributeTargets.Property)] -public class StorageAttribute : Attribute -{ - public StorageMode Mode { get; set; } - public StorageAttribute(StorageMode mode) - { - Mode = mode; - } -} diff --git a/Libraries/Esiur/Resource/PropertyPermission.cs b/Libraries/Esiur/Resource/PropertyPermission.cs index 2d72b06..3becb6f 100644 --- a/Libraries/Esiur/Resource/PropertyPermission.cs +++ b/Libraries/Esiur/Resource/PropertyPermission.cs @@ -4,10 +4,10 @@ using System.Text; namespace Esiur.Resource { - //public enum PropertyPermission : byte - //{ - // ReadOnly = 0, - // Write, - // ReadWrite, - //} + public enum PropertyPermission : byte + { + Read = 1, + Write, + ReadWrite, + } } diff --git a/Tests/Unit/IndexedStructureTests.cs b/Tests/Unit/IndexedStructureTests.cs new file mode 100644 index 0000000..052148b --- /dev/null +++ b/Tests/Unit/IndexedStructureTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using Esiur.Data; +using Esiur.Resource; + +namespace Esiur.Tests.Unit; + +public class IndexedStructureTests +{ + private sealed class ExampleInfo : IndexedStructure + { + [Index(1)] + public int Count { get; set; } + + [Index(2)] + public string? Note { get; set; } + } + + private sealed class MemberDefInfo : IndexedStructure + { + [Index(1)] + public string? Name { get; set; } + + [Index(2)] + public ExampleInfo? Example { get; set; } + + [Index(3)] + public List? Examples { get; set; } + } + + [Fact] + public void NestedStructure_RoundTripsDirectly() + { + var source = new MemberDefInfo + { + Name = "length", + Example = new ExampleInfo { Count = 3, Note = "nested" }, + Examples = new List + { + new() { Count = 4, Note = "first" }, + new() { Count = 5, Note = "second" }, + }, + }; + + var bytes = Codec.ComposeIndexedType(source, Warehouse.Default, null); + var (_, raw) = Codec.ParseSync(bytes, 0, Warehouse.Default); + var (size, parsed) = Codec.ParseIndexedType(bytes, 0, Warehouse.Default); + + Assert.Equal((uint)bytes.Length, size); + Assert.IsType>(raw); + Assert.Equal("length", parsed.Name); + Assert.NotNull(parsed.Example); + Assert.Equal(3, parsed.Example.Count); + Assert.Equal("nested", parsed.Example.Note); + Assert.Equal(2, parsed.Examples!.Count); + Assert.Equal(5, parsed.Examples[1].Count); + } + + [Fact] + public void UnknownIndexes_AreIgnored() + { + var map = new Map + { + [1] = "known", + [99] = "future field", + }; + + var parsed = Codec.ParseIndexedType(map); + + Assert.Equal("known", parsed.Name); + Assert.Null(parsed.Example); + } + + [Fact] + public void DuplicateIndexes_AreRejected() + { + var value = new InvalidStructure(); + var error = Assert.Throws(() => + Codec.ComposeIndexedType(value, Warehouse.Default, null)); + + Assert.Contains("index 1", error.Message, StringComparison.OrdinalIgnoreCase); + } + + private sealed class InvalidStructure : IndexedStructure + { + [Index(1)] public int First { get; set; } + [Index(1)] public int Second { get; set; } + } +} diff --git a/Tests/Unit/TypeDefInfoSerializationTests.cs b/Tests/Unit/TypeDefInfoSerializationTests.cs new file mode 100644 index 0000000..7e6f472 --- /dev/null +++ b/Tests/Unit/TypeDefInfoSerializationTests.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using Esiur.Data; +using Esiur.Data.Types; +using Esiur.Resource; + +namespace Esiur.Tests.Unit; + +public class TypeDefInfoSerializationTests +{ + [Fact] + public void Tru_HasDedicatedTduAndRoundTrips() + { + Tru source = new TruPrimitive(TruIdentifier.String, true, typeof(string)); + + var bytes = Codec.Compose(source, Warehouse.Default, null); + var (size, value) = Codec.ParseSync(bytes, 0, Warehouse.Default); + + Assert.Equal((byte)TduIdentifier.TRU, (byte)(bytes[0] & 0xC7)); + Assert.Equal((uint)bytes.Length, size); + var parsed = Assert.IsAssignableFrom(value); + Assert.Equal(TruIdentifier.String, parsed.Identifier); + Assert.True(parsed.Nullable); + } + + [Fact] + public void ConsecutiveTrus_UseContinuationAndRoundTrip() + { + var source = new object[] + { + new TruPrimitive(TruIdentifier.String, false, typeof(string)), + new TruPrimitive(TruIdentifier.Int32, false, typeof(int)), + }; + + var bytes = Codec.Compose(source, Warehouse.Default, null); + var (_, value) = Codec.ParseSync(bytes, 0, Warehouse.Default); + var parsed = Assert.IsType(value); + + Assert.Equal(TruIdentifier.String, Assert.IsAssignableFrom(parsed[0]).Identifier); + Assert.Equal(TruIdentifier.Int32, Assert.IsAssignableFrom(parsed[1]).Identifier); + } + + [Fact] + public void TypeDefInfo_WithNestedMembersAndTrus_RoundTrips() + { + var source = new TypeDefInfo + { + Version = 3, + Id = 42, + Name = "Widget", + Namespace = "Example.Models", + Kind = TypeDefKind.Resource, + Parent = 7, + Description = "A test resource", + Annotations = new Map { ["audience"] = "test" }, + Properties = new List + { + new() + { + Index = 1, + Name = "Title", + Flags = (byte)PropertyDefFlags.ReadOnly, + ValueType = new TruPrimitive(TruIdentifier.String, false, typeof(string)), + OrderingControl = OrderingControl.LatestOnly, + }, + }, + Functions = new List + { + new() + { + Index = 2, + Name = "Find", + Flags = (byte)FunctionDefFlags.Idempotent, + ReturnType = new TruPrimitive(TruIdentifier.String, true, typeof(string)), + Arguments = new List + { + new() + { + Index = 0, + Name = "id", + ValueType = new TruPrimitive(TruIdentifier.UInt64, false, typeof(ulong)), + }, + }, + }, + }, + Events = new List + { + new() + { + Index = 3, + Name = "Changed", + ArgumentName = "title", + ArgumentType = new TruPrimitive(TruIdentifier.String, false, typeof(string)), + }, + }, + Constants = new List + { + new() + { + Index = 4, + Name = "Maximum", + ValueType = new TruPrimitive(TruIdentifier.Int32, false, typeof(int)), + Value = 100, + }, + }, + }; + + var bytes = Codec.Compose(source, Warehouse.Default, null); + var (size, parsed) = Codec.ParseIndexedType(bytes, 0, Warehouse.Default); + + Assert.Equal((byte)TduIdentifier.TypeDef, (byte)(bytes[0] & 0xC7)); + Assert.Equal((uint)bytes.Length, size); + Assert.Equal(source.Id, parsed.Id); + Assert.Equal("Widget", parsed.Name); + Assert.Equal(TypeDefKind.Resource, parsed.Kind); + Assert.Equal("test", parsed.Annotations!["audience"]); + + var property = Assert.Single(parsed.Properties!); + Assert.Equal("Title", property.Name); + Assert.Equal(TruIdentifier.String, property.ValueType.Identifier); + Assert.Equal(OrderingControl.LatestOnly, property.OrderingControl); + + var function = Assert.Single(parsed.Functions!); + Assert.True(function.ReturnType.Nullable); + Assert.Equal(TruIdentifier.String, function.ReturnType.Identifier); + Assert.Equal(TruIdentifier.UInt64, Assert.Single(function.Arguments!).ValueType.Identifier); + + Assert.Equal(TruIdentifier.String, Assert.Single(parsed.Events!).ArgumentType.Identifier); + Assert.Equal(100, Convert.ToInt32(Assert.Single(parsed.Constants!).Value)); + } +}