mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
IndexedStructure
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// Detect anonymous types
|
||||
|
||||
@@ -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<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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes an indexed CLR structure using the compatible Map<byte, object> 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)
|
||||
{
|
||||
return new Tdu(TduIdentifier.UUID, ((Uuid)value).Data, 16, null, null);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<byte, object></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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -46,49 +46,38 @@ public class FunctionDef : MemberDef
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public static async AsyncReply<ParseResult<FunctionDef>> ParseAsync(byte[] data, uint offset, byte index, bool inherited, EpConnection connection, ulong[] requestSequence)
|
||||
public static async AsyncReply<ParseResult<FunctionDef>> 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<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);
|
||||
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<string, string> annotations = null;
|
||||
|
||||
// arguments
|
||||
if (hasAnnotation) // Annotation ?
|
||||
if (hasAnnotations)
|
||||
{
|
||||
var (len, anns) = Codec.ParseSync(data, offset, null);
|
||||
|
||||
if (anns is Map<string, string> map)
|
||||
annotations = map;
|
||||
|
||||
offset += len;
|
||||
var (size, value) = Codec.ParseSync(data, offset, null);
|
||||
annotations = value as Map<string, string>;
|
||||
offset += size;
|
||||
}
|
||||
|
||||
return new ParseResult<FunctionDef>( new FunctionDef()
|
||||
return new ParseResult<FunctionDef>(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<ParseResult<FunctionDef>> 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<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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<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
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
//public enum PropertyPermission : byte
|
||||
//{
|
||||
// ReadOnly = 0,
|
||||
// Write,
|
||||
// ReadWrite,
|
||||
//}
|
||||
public enum PropertyPermission : byte
|
||||
{
|
||||
Read = 1,
|
||||
Write,
|
||||
ReadWrite,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user