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));
+ }
+}