IndexedStructure

This commit is contained in:
2026-07-13 08:39:52 +03:00
parent 2f81cd1d30
commit 31bda58460
26 changed files with 1381 additions and 87 deletions
+54 -1
View File
@@ -102,6 +102,8 @@ public static class Codec
static AsyncParser[] TypedAsyncParsers = new AsyncParser[] static AsyncParser[] TypedAsyncParsers = new AsyncParser[]
{ {
DataDeserializer.TypedParserAsync, DataDeserializer.TypedParserAsync,
DataDeserializer.TypeDefInfoParserAsync,
DataDeserializer.TruParserAsync,
}; };
static AsyncParser[] ExtendedAsyncParsers = new AsyncParser[] static AsyncParser[] ExtendedAsyncParsers = new AsyncParser[]
@@ -168,6 +170,8 @@ public static class Codec
static SyncParser[] TypedParsers = new SyncParser[] static SyncParser[] TypedParsers = new SyncParser[]
{ {
DataDeserializer.TypedParser, DataDeserializer.TypedParser,
DataDeserializer.TypeDefInfoParser,
DataDeserializer.TruParser,
//DataDeserializer.RecordParser, //DataDeserializer.RecordParser,
//DataDeserializer.TypedListParser, //DataDeserializer.TypedListParser,
//DataDeserializer.TypedMapParser, //DataDeserializer.TypedMapParser,
@@ -541,7 +545,15 @@ public static class Codec
type = valueOrSource.GetType(); 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); return Composers[type](valueOrSource, warehouse, connection);
} }
@@ -555,6 +567,10 @@ public static class Codec
{ {
return DataSerializer.RecordComposer(valueOrSource, warehouse, connection); return DataSerializer.RecordComposer(valueOrSource, warehouse, connection);
} }
else if (valueOrSource is IndexedStructure)
{
return DataSerializer.StructureComposer(valueOrSource, warehouse, connection);
}
else if (type.IsGenericType) else if (type.IsGenericType)
{ {
var genericType = type.GetGenericTypeDefinition(); var genericType = type.GetGenericTypeDefinition();
@@ -636,6 +652,43 @@ public static class Codec
return tdu.Composed; return tdu.Composed;
} }
/// <summary>
/// Encodes a local indexed structure. This is equivalent to <see cref="Compose"/>, but
/// makes the expected structure type explicit at call sites such as TypeDef and authentication.
/// </summary>
public static byte[] ComposeIndexedType<T>(T value, Warehouse warehouse, EpConnection connection)
where T : IndexedStructure
=> Compose(value, warehouse, connection);
/// <summary>
/// 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.
/// </summary>
public static (uint Size, T Value) ParseIndexedType<T>(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<T>(value));
}
/// <summary>
/// Parses an already framed TDU as an indexed structure.
/// </summary>
public static T ParseIndexedType<T>(PlainTdu tdu, Warehouse warehouse)
where T : IndexedStructure
{
var value = ParseSync(tdu, warehouse);
return value is T typed ? typed : IndexedStructureCodec.FromMap<T>(value);
}
/// <summary>
/// Converts an indexed map already decoded by the codec to a typed structure without
/// reparsing any wire data.
/// </summary>
public static T ParseIndexedType<T>(object indexedMap)
where T : IndexedStructure
=> indexedMap is T typed ? typed : IndexedStructureCodec.FromMap<T>(indexedMap);
public static bool IsAnonymous(Type type) public static bool IsAnonymous(Type type)
{ {
// Detect anonymous types // Detect anonymous types
+50
View File
@@ -11,14 +11,57 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.ComTypes; using System.Runtime.InteropServices.ComTypes;
using System.Text; using System.Text;
using System.Threading;
namespace Esiur.Data; namespace Esiur.Data;
public static class DataDeserializer public static class DataDeserializer
{ {
internal static readonly AsyncLocal<ulong[]> TypeDefRequestSequence = new();
public static async AsyncReply<object> 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<Types.TypeDefInfo>(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<Types.TypeDefInfo>(value);
}
public static async AsyncReply<object> 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) public static object NullParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
{ {
return null; return null;
@@ -445,6 +488,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
else if (current.Identifier == TduIdentifier.TypeOfTarget) else if (current.Identifier == TduIdentifier.TypeOfTarget)
@@ -596,6 +640,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
else if (current.Identifier == TduIdentifier.TypeOfTarget) else if (current.Identifier == TduIdentifier.TypeOfTarget)
@@ -908,6 +953,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
@@ -959,6 +1005,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
@@ -1163,6 +1210,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
else if (current.Identifier == TduIdentifier.TypeOfTarget) else if (current.Identifier == TduIdentifier.TypeOfTarget)
@@ -1271,6 +1319,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
} }
@@ -1624,6 +1673,7 @@ public static class DataDeserializer
{ {
current.Class = previous.Value.Class; current.Class = previous.Value.Class;
current.Identifier = previous.Value.Identifier; current.Identifier = previous.Value.Identifier;
current.Index = previous.Value.Index;
current.Metadata = previous.Value.Metadata; current.Metadata = previous.Value.Metadata;
} }
else if (current.Identifier == TduIdentifier.TypeOfTarget) else if (current.Identifier == TduIdentifier.TypeOfTarget)
+44 -2
View File
@@ -567,9 +567,13 @@ public static class DataSerializer
{ {
var tdu = Codec.ComposeInternal(i, warehouse, connection); 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, var d = tdu.Composed.Clip(tdu.ContentOffset,
(uint)tdu.Composed.Length - tdu.ContentOffset); (uint)tdu.Composed.Length - tdu.ContentOffset);
@@ -605,6 +609,12 @@ public static class DataSerializer
{ {
var elementTru = Tru.FromType(type, warehouse); 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); byte[] composed = TypedArrayComposer(value, elementTru, warehouse, connection);
if (composed == null) if (composed == null)
@@ -859,6 +869,38 @@ public static class DataSerializer
return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null); return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null);
} }
/// <summary>
/// Composes an indexed CLR structure using the compatible Map&lt;byte, object&gt; wire shape.
/// </summary>
public static Tdu StructureComposer(object value, Warehouse warehouse, EpConnection connection)
{
if (value == null)
return new Tdu(TduIdentifier.Null, Array.Empty<byte>(), 0, null, null);
var map = IndexedStructureCodec.ToMap((IndexedStructure)value);
return TypedMapComposer(map, typeof(byte), typeof(object), warehouse, connection);
}
/// <summary>
/// Carries a TRU as a self-framed value, allowing it to appear in dynamic structure fields.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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) public static unsafe Tdu UUIDComposer(object value, Warehouse warehouse, EpConnection connection)
{ {
return new Tdu(TduIdentifier.UUID, ((Uuid)value).Data, 16, null, null); return new Tdu(TduIdentifier.UUID, ((Uuid)value).Data, 16, null, null);
+29
View File
@@ -0,0 +1,29 @@
using System;
namespace Esiur.Data;
/// <summary>
/// Assigns a stable wire index to a field or property of a <see cref="IndexedStructure"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
AllowMultiple = false, Inherited = true)]
public sealed class IndexAttribute : Attribute
{
/// <summary>
/// Gets or sets the byte-sized index used on the wire.
/// </summary>
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;
}
}
+21
View File
@@ -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;
/// <summary>
/// Base class for local, schema-less types whose members are identified by byte indexes.
/// Structures use the same wire representation as <c>Map&lt;byte, object&gt;</c>, but can be
/// composed and parsed directly as CLR types through <see cref="Codec"/>.
/// </summary>
public abstract class IndexedStructure
{
}
@@ -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<object, object?> Get { get; }
public Action<object, object?> 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<Type, IndexedMember[]> Members = new();
internal static Map<byte, object?> ToMap(IndexedStructure value)
{
if (value is null)
throw new ArgumentNullException(nameof(value));
var map = new Map<byte, object?>();
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<T>(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<IndexedMember>();
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
foreach (var property in type.GetProperties(flags))
{
var attribute = property.GetCustomAttribute<IndexAttribute>(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<IndexAttribute>(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<object?>().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<object?>().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;
}
}
}
+3 -3
View File
@@ -75,7 +75,7 @@ namespace Esiur.Data
Ends = ends Ends = ends
}; };
} }
else if (cls == TduClass.Typed) else if ((h & 0xC7) == 0x80) // cls == TduClass = Typed & Identifier = Typed
{ {
ulong cll = (ulong)(h >> 3) & 0x7; ulong cll = (ulong)(h >> 3) & 0x7;
@@ -206,7 +206,7 @@ namespace Esiur.Data
Ends = ends Ends = ends
}; };
} }
else if (cls == TduClass.Typed) else if ((h & 0xC7) == 0x80) // else if (cls == TduClass.Typed)
{ {
ulong cll = (ulong)(h >> 3) & 0x7; ulong cll = (ulong)(h >> 3) & 0x7;
@@ -395,7 +395,7 @@ namespace Esiur.Data
Ends = ends Ends = ends
}; };
} }
else if (cls == TduClass.Typed) else if ((h & 0xC7) == 0x80) // (cls == TduClass.Typed)
{ {
ulong cll = (ulong)(h >> 3) & 0x7; ulong cll = (ulong)(h >> 3) & 0x7;
+1 -1
View File
@@ -75,7 +75,7 @@ namespace Esiur.Data
Ends = ends Ends = ends
}; };
} }
else if (cls == TduClass.Typed) else if ((h & 0xC7) == 0x80) // else if (cls == TduClass.Typed)
{ {
ulong cll = (ulong)(h >> 3) & 0x7; ulong cll = (ulong)(h >> 3) & 0x7;
+10 -1
View File
@@ -81,7 +81,8 @@ public struct Tdu
Composed = DC.Combine(new byte[] { (byte)Identifier }, 0, 1, data, 0, (uint)length); Composed = DC.Combine(new byte[] { (byte)Identifier }, 0, 1, data, 0, (uint)length);
} }
else if (Class == TduClass.Dynamic else if (Class == TduClass.Dynamic
|| Class == TduClass.Extension) || Class == TduClass.Extension
|| (Class == TduClass.Typed && Identifier != TduIdentifier.Typed))
{ {
if (length == 0) if (length == 0)
@@ -287,6 +288,14 @@ public struct Tdu
if (Class != TduClass.Typed || with.Class != TduClass.Typed) if (Class != TduClass.Typed || with.Class != TduClass.Typed)
return false; 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)) if (!Metadata.Match(with.Metadata))
return false; return false;
+2 -7
View File
@@ -51,16 +51,11 @@ namespace Esiur.Data
MapList = 0x47, MapList = 0x47,
Typed = 0x80, Typed = 0x80,
TypeDef = 0x81,
TRU = 0x82,
//Record = 0x80,
//TypedList = 0x81,
//TypedMap = 0x82,
//TypedTuple = 0x83,
//TypedEnum = 0x84,
//TypedConstant = 0x85,
TypeContinuation = 0xC0, TypeContinuation = 0xC0,
TypeOfTarget = 0xC1, TypeOfTarget = 0xC1,
TypeDef = 0xC2,
} }
} }
@@ -0,0 +1,15 @@
#nullable enable
namespace Esiur.Data.Types;
/// <summary>
/// Indexed function-argument definition carried inside <see cref="FunctionDefInfo"/>.
/// </summary>
public class ArgumentDefInfo : MemberDefInfo
{
[Index((int)ArgumentDefField.ValueType)]
public Tru ValueType { get; set; } = null!;
[Index((int)ArgumentDefField.DefaultValue)]
public object? DefaultValue { get; set; }
}
@@ -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; }
}
+39
View File
@@ -28,12 +28,51 @@ public class EventDef : MemberDef
public bool AutoDelivered { get; set; } 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 bool Deprecated { get; set; }
public EventInfo EventInfo { get; set; } public EventInfo EventInfo { get; set; }
public Tru ArgumentType { get; set; } public Tru ArgumentType { get; set; }
public static async AsyncReply<ParseResult<EventDef>> 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<string, string> annotations = null;
if (hasAnnotations)
{
var (size, value) = Codec.ParseSync(data, offset, null);
annotations = value as Map<string, string>;
offset += size;
}
return new ParseResult<EventDef>(new EventDef
{
Index = index,
Name = name,
Inherited = inherited,
ArgumentType = argumentType.Value,
Subscribable = subscribable,
Annotations = annotations,
}, offset - originalOffset);
}
//public static async AsyncReply<ParseResult<EventDef>> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence) //public static async AsyncReply<ParseResult<EventDef>> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence)
@@ -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; }
}
+93 -50
View File
@@ -46,49 +46,38 @@ public class FunctionDef : MemberDef
set; set;
} }
public static async AsyncReply<ParseResult<FunctionDef>> ParseAsync(
public static async AsyncReply<ParseResult<FunctionDef>> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence) byte[] data, uint offset, byte index, bool inherited,
EpConnection connection, ulong[] requestSequence)
{ {
var originalOffset = offset;
var oOffset = offset; var isStatic = (data[offset] & 0x04) != 0;
var hasAnnotations = (data[offset++] & 0x10) != 0;
var isStatic = ((data[offset] & 0x4) == 0x4);
var hasAnnotation = ((data[offset++] & 0x10) == 0x10);
var name = data.GetString(offset + 1, data[offset]); var name = data.GetString(offset + 1, data[offset]);
offset += (uint)data[offset] + 1; offset += (uint)data[offset] + 1;
//Console.WriteLine("Parsing functionDef " + name);
// return type
var returnType = await Tru.ParseAsync(data, offset, connection, requestSequence); var returnType = await Tru.ParseAsync(data, offset, connection, requestSequence);
offset += returnType.Size; offset += returnType.Size;
// arguments count
var argsCount = data[offset++];
List<ArgumentDef> arguments = new();
for (var a = 0; a < argsCount; a++) var argumentCount = data[offset++];
var arguments = new List<ArgumentDef>(argumentCount);
for (var argumentIndex = 0; argumentIndex < argumentCount; argumentIndex++)
{ {
var argType = await ArgumentDef.ParseAsync(data, offset, a, connection, requestSequence); var argument = await ArgumentDef.ParseAsync(
arguments.Add(argType.Value); data, offset, argumentIndex, connection, requestSequence);
offset += argType.Size; arguments.Add(argument.Value);
offset += argument.Size;
} }
Map<string, string> annotations = null; Map<string, string> annotations = null;
if (hasAnnotations)
// arguments
if (hasAnnotation) // Annotation ?
{ {
var (len, anns) = Codec.ParseSync(data, offset, null); var (size, value) = Codec.ParseSync(data, offset, null);
annotations = value as Map<string, string>;
if (anns is Map<string, string> map) offset += size;
annotations = map;
offset += len;
} }
return new ParseResult<FunctionDef>( new FunctionDef() return new ParseResult<FunctionDef>(new FunctionDef
{ {
Index = index, Index = index,
Name = name, Name = name,
@@ -97,35 +86,89 @@ public class FunctionDef : MemberDef
Inherited = inherited, Inherited = inherited,
Annotations = annotations, Annotations = annotations,
ReturnType = returnType.Value, ReturnType = returnType.Value,
}, offset - oOffset); }, offset - originalOffset);
} }
public byte[] Compose(EpConnection connection)
{
var name = DC.ToBytes(Name); //public static async AsyncReply<ParseResult<FunctionDef>> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence)
//{
var bl = new BinaryList() // var oOffset = offset;
.AddUInt8((byte)name.Length)
.AddUInt8Array(name)
.AddUInt8Array(ReturnType.Compose(connection))
.AddUInt8((byte)Arguments.Length);
for (var i = 0; i < Arguments.Length; i++) // var isStatic = ((data[offset] & 0x4) == 0x4);
bl.AddUInt8Array(Arguments[i].Compose(connection)); // 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<ArgumentDef> 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<string, string> annotations = null;
// // arguments
// if (hasAnnotation) // Annotation ?
// {
// var (len, anns) = Codec.ParseSync(data, offset, null);
// if (anns is Map<string, string> map)
// annotations = map;
// offset += len;
// }
// return new ParseResult<FunctionDef>( 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) // if (Annotations != null)
{ // {
var exp = Codec.Compose(Annotations, connection.Instance.Warehouse , connection);// DC.ToBytes(Annotation); // var exp = Codec.Compose(Annotations, connection.Instance.Warehouse , connection);// DC.ToBytes(Annotation);
bl.AddUInt8Array(exp); // bl.AddUInt8Array(exp);
bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0))); // bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0)));
} // }
else // else
bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x80 : (byte)0x0) | (IsStatic ? 0x4 : 0))); // 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) public static FunctionDef MakeFunctionDef(Warehouse warehouse, Type type, MethodInfo mi, byte index, string name, TypeDef schema)
@@ -0,0 +1,16 @@
#nullable enable
using System.Collections.Generic;
namespace Esiur.Data.Types;
public class FunctionDefInfo : MemberDefInfo
{
[Index((int)FunctionDefField.Arguments)]
public List<ArgumentDefInfo>? Arguments { get; set; }
[Index((int)FunctionDefField.ReturnType)]
public Tru ReturnType { get; set; } = null!;
[Index((int)FunctionDefField.StreamMode)]
public StreamMode StreamMode { get; set; }
}
@@ -0,0 +1,71 @@
#nullable enable
using System.Collections.Generic;
namespace Esiur.Data.Types;
/// <summary>
/// Wire representation shared by all TypeDef members.
/// </summary>
public class MemberDefInfo : IndexedStructure
{
[Index((int)MemberDefField.Index)]
public byte Index { get; set; }
[Index((int)MemberDefField.Name)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Raw member-specific flags. Interpret this as PropertyDefFlags, FunctionDefFlags,
/// EventDefFlags, ConstantDefFlags, or ArgumentDefFlags according to the concrete type.
/// </summary>
[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<object>? Examples { get; set; }
[Index((int)MemberDefField.Tags)]
public List<string>? 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<object>? 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<string>? Preconditions { get; set; }
[Index((int)MemberDefField.Postconditions)]
public List<string>? Postconditions { get; set; }
[Index((int)MemberDefField.Effects)]
public OperationEffects Effects { get; set; }
[Index((int)MemberDefField.Warnings)]
public List<string>? Warnings { get; set; }
[Index((int)MemberDefField.RelatedMembers)]
public List<byte>? RelatedMembers { get; set; }
[Index((int)MemberDefField.DeprecationMessage)]
public string? DeprecationMessage { get; set; }
}
+56
View File
@@ -44,11 +44,58 @@ public class PropertyDef : MemberDef
public bool ReadOnly { get; set; } 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 Constant { get; set; }
//public bool IsNullable { get; set; } //public bool IsNullable { get; set; }
public bool Historical { get; set; } public bool Historical { get; set; }
public bool HasHistory
{
get => Historical;
set => Historical = value;
}
public static async AsyncReply<ParseResult<PropertyDef>> 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<string, string> annotations = null;
if (hasAnnotations)
{
var (size, value) = Codec.ParseSync(data, offset, null);
annotations = value as Map<string, string>;
offset += size;
}
return new ParseResult<PropertyDef>(new PropertyDef
{
Index = index,
Name = name,
Inherited = inherited,
Permission = permission,
HasHistory = hasHistory,
ValueType = valueType.Value,
Annotations = annotations,
}, offset - originalOffset);
}
/* /*
public PropertyType Mode 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) public static string GetTypeAnnotationName(Type type)
{ {
@@ -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; }
}
+154
View File
@@ -44,6 +44,26 @@ public class RemoteTypeDef:TypeDef
{ {
uint ends = offset + contentLength; 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; uint oOffset = offset;
// start parsing... // start parsing...
@@ -161,5 +181,139 @@ public class RemoteTypeDef:TypeDef
return od; 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<T>(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<ArgumentDef>(),
});
}
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);
}
} }
+4 -1
View File
@@ -46,6 +46,8 @@ public class TypeDef
public TypeDefKind Kind => _typeDefKind; public TypeDefKind Kind => _typeDefKind;
public int Version => _version;
public EventDef GetEventDefByName(string eventName) public EventDef GetEventDefByName(string eventName)
{ {
foreach (var i in _events) foreach (var i in _events)
@@ -151,7 +153,8 @@ public class TypeDef
public virtual byte[] Compose(EpConnection connection) public virtual byte[] Compose(EpConnection connection)
{ {
return null; var warehouse = connection?.Instance?.Warehouse ?? Warehouse.Default;
return Codec.Compose(TypeDefInfo.FromTypeDef(this), warehouse, connection);
} }
+168
View File
@@ -0,0 +1,168 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
namespace Esiur.Data.Types;
/// <summary>
/// Indexed, transport-safe representation of a type definition.
/// </summary>
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<PropertyDefInfo>? Properties { get; set; }
[Index((int)TypeDefField.Functions)]
public List<FunctionDefInfo>? Functions { get; set; }
[Index((int)TypeDefField.Events)]
public List<EventDefInfo>? Events { get; set; }
[Index((int)TypeDefField.Constants)]
public List<ConstantDefInfo>? 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<string, string>? 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<T>(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,
});
}
}
@@ -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;
}
}
@@ -4,10 +4,10 @@ using System.Text;
namespace Esiur.Resource namespace Esiur.Resource
{ {
//public enum PropertyPermission : byte public enum PropertyPermission : byte
//{ {
// ReadOnly = 0, Read = 1,
// Write, Write,
// ReadWrite, ReadWrite,
//} }
} }
+89
View File
@@ -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<ExampleInfo>? Examples { get; set; }
}
[Fact]
public void NestedStructure_RoundTripsDirectly()
{
var source = new MemberDefInfo
{
Name = "length",
Example = new ExampleInfo { Count = 3, Note = "nested" },
Examples = new List<ExampleInfo>
{
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<MemberDefInfo>(bytes, 0, Warehouse.Default);
Assert.Equal((uint)bytes.Length, size);
Assert.IsType<Map<byte, object>>(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<byte, object>
{
[1] = "known",
[99] = "future field",
};
var parsed = Codec.ParseIndexedType<MemberDefInfo>(map);
Assert.Equal("known", parsed.Name);
Assert.Null(parsed.Example);
}
[Fact]
public void DuplicateIndexes_AreRejected()
{
var value = new InvalidStructure();
var error = Assert.Throws<InvalidOperationException>(() =>
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; }
}
}
+130
View File
@@ -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<Tru>(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<object[]>(value);
Assert.Equal(TruIdentifier.String, Assert.IsAssignableFrom<Tru>(parsed[0]).Identifier);
Assert.Equal(TruIdentifier.Int32, Assert.IsAssignableFrom<Tru>(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<string, string> { ["audience"] = "test" },
Properties = new List<PropertyDefInfo>
{
new()
{
Index = 1,
Name = "Title",
Flags = (byte)PropertyDefFlags.ReadOnly,
ValueType = new TruPrimitive(TruIdentifier.String, false, typeof(string)),
OrderingControl = OrderingControl.LatestOnly,
},
},
Functions = new List<FunctionDefInfo>
{
new()
{
Index = 2,
Name = "Find",
Flags = (byte)FunctionDefFlags.Idempotent,
ReturnType = new TruPrimitive(TruIdentifier.String, true, typeof(string)),
Arguments = new List<ArgumentDefInfo>
{
new()
{
Index = 0,
Name = "id",
ValueType = new TruPrimitive(TruIdentifier.UInt64, false, typeof(ulong)),
},
},
},
},
Events = new List<EventDefInfo>
{
new()
{
Index = 3,
Name = "Changed",
ArgumentName = "title",
ArgumentType = new TruPrimitive(TruIdentifier.String, false, typeof(string)),
},
},
Constants = new List<ConstantDefInfo>
{
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<TypeDefInfo>(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));
}
}