using ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.V1;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol;
///
/// CLR value ↔ (design §4.4). The exact boxed CLR type survives the round trip — a script
/// doing (short)ctx.GetTag(x).Value throws if a short arrives as an
/// int — so the type tag names the CLR type, not just an OPC UA one. A type with no tag is never converted:
/// refuses it, and the caller reports UNSUPPORTED_TYPE (design §14 #11).
///
public static class VariantCodec
{
/// Encodes .
/// The boxed CLR value (null allowed).
/// The encoded variant when supported.
/// The CLR type name when not supported.
/// true when the type has a tag.
public static bool TryEncode(object? value, out Variant variant, out string? unsupportedType)
{
unsupportedType = null;
variant = new Variant();
switch (value)
{
case null: variant.Type = Variant.Types.TypeTag.Null; return true;
case bool b: variant.Type = Variant.Types.TypeTag.Boolean; variant.B = b; return true;
case sbyte v: variant.Type = Variant.Types.TypeTag.Sbyte; variant.I = v; return true;
case byte v: variant.Type = Variant.Types.TypeTag.Byte; variant.U = v; return true;
case short v: variant.Type = Variant.Types.TypeTag.Int16; variant.I = v; return true;
case ushort v: variant.Type = Variant.Types.TypeTag.Uint16; variant.U = v; return true;
case int v: variant.Type = Variant.Types.TypeTag.Int32; variant.I = v; return true;
case uint v: variant.Type = Variant.Types.TypeTag.Uint32; variant.U = v; return true;
case long v: variant.Type = Variant.Types.TypeTag.Int64; variant.I = v; return true;
case ulong v: variant.Type = Variant.Types.TypeTag.Uint64; variant.U = v; return true;
case float v: variant.Type = Variant.Types.TypeTag.Float; variant.F = v; return true;
case double v: variant.Type = Variant.Types.TypeTag.Double; variant.D = v; return true;
case string v: variant.Type = Variant.Types.TypeTag.String; variant.S = v; return true;
case DateTime v:
variant.Type = Variant.Types.TypeTag.DatetimeUtc;
variant.DtTicks = ToUtc(v).Ticks;
return true;
case Array array when array.Rank == 1 && TryElementTag(array.GetType().GetElementType()!, out var elementTag):
{
var av = new ArrayValue { ElementType = elementTag };
foreach (var item in array)
{
if (!TryEncode(item, out var element, out unsupportedType)) return false;
av.Items.Add(element);
}
variant.Type = Variant.Types.TypeTag.Array;
variant.Array = av;
return true;
}
default:
unsupportedType = value.GetType().FullName ?? value.GetType().Name;
return false;
}
}
/// Decodes back to the boxed CLR value it was encoded from.
/// The variant.
/// The boxed value.
/// An unknown tag, or a payload that does not match the tag.
public static object? Decode(Variant? variant)
{
if (variant is null) return null;
return variant.Type switch
{
Variant.Types.TypeTag.Null => null,
Variant.Types.TypeTag.Boolean => Expect(variant, Variant.VOneofCase.B).B,
Variant.Types.TypeTag.Sbyte => checked((sbyte)Expect(variant, Variant.VOneofCase.I).I),
Variant.Types.TypeTag.Byte => checked((byte)Expect(variant, Variant.VOneofCase.U).U),
Variant.Types.TypeTag.Int16 => checked((short)Expect(variant, Variant.VOneofCase.I).I),
Variant.Types.TypeTag.Uint16 => checked((ushort)Expect(variant, Variant.VOneofCase.U).U),
Variant.Types.TypeTag.Int32 => checked((int)Expect(variant, Variant.VOneofCase.I).I),
Variant.Types.TypeTag.Uint32 => checked((uint)Expect(variant, Variant.VOneofCase.U).U),
Variant.Types.TypeTag.Int64 => Expect(variant, Variant.VOneofCase.I).I,
Variant.Types.TypeTag.Uint64 => Expect(variant, Variant.VOneofCase.U).U,
Variant.Types.TypeTag.Float => Expect(variant, Variant.VOneofCase.F).F,
Variant.Types.TypeTag.Double => Expect(variant, Variant.VOneofCase.D).D,
Variant.Types.TypeTag.String => Expect(variant, Variant.VOneofCase.S).S,
Variant.Types.TypeTag.DatetimeUtc => new DateTime(Expect(variant, Variant.VOneofCase.DtTicks).DtTicks, DateTimeKind.Utc),
Variant.Types.TypeTag.Array => DecodeArray(Expect(variant, Variant.VOneofCase.Array).Array),
_ => throw new ScriptWorkerProtocolException($"unknown variant type tag {(int)variant.Type}"),
};
}
/// A as UTC: a Local value is converted; an Unspecified one is taken as UTC.
/// The value.
/// The UTC value.
public static DateTime ToUtc(DateTime value) => value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc),
};
private static Variant Expect(Variant variant, Variant.VOneofCase expected)
{
// proto3 drops a default scalar (0, false, "") from the oneof, so an absent payload is the default value.
if (variant.VCase != expected && variant.VCase != Variant.VOneofCase.None)
throw new ScriptWorkerProtocolException($"variant tagged {variant.Type} carries a {variant.VCase} payload");
return variant;
}
private static Array DecodeArray(ArrayValue? array)
{
if (array is null) throw new ScriptWorkerProtocolException("array variant without an array payload");
var elementType = ClrType(array.ElementType);
var result = Array.CreateInstance(elementType, array.Items.Count);
for (var i = 0; i < array.Items.Count; i++)
{
var item = array.Items[i];
if (item.Type != array.ElementType && item.Type != Variant.Types.TypeTag.Null)
throw new ScriptWorkerProtocolException($"array of {array.ElementType} holds a {item.Type} element");
result.SetValue(Decode(item), i);
}
return result;
}
private static bool TryElementTag(Type elementType, out Variant.Types.TypeTag tag)
{
tag = elementType switch
{
_ when elementType == typeof(bool) => Variant.Types.TypeTag.Boolean,
_ when elementType == typeof(sbyte) => Variant.Types.TypeTag.Sbyte,
_ when elementType == typeof(byte) => Variant.Types.TypeTag.Byte,
_ when elementType == typeof(short) => Variant.Types.TypeTag.Int16,
_ when elementType == typeof(ushort) => Variant.Types.TypeTag.Uint16,
_ when elementType == typeof(int) => Variant.Types.TypeTag.Int32,
_ when elementType == typeof(uint) => Variant.Types.TypeTag.Uint32,
_ when elementType == typeof(long) => Variant.Types.TypeTag.Int64,
_ when elementType == typeof(ulong) => Variant.Types.TypeTag.Uint64,
_ when elementType == typeof(float) => Variant.Types.TypeTag.Float,
_ when elementType == typeof(double) => Variant.Types.TypeTag.Double,
_ when elementType == typeof(string) => Variant.Types.TypeTag.String,
_ when elementType == typeof(DateTime) => Variant.Types.TypeTag.DatetimeUtc,
_ => Variant.Types.TypeTag.Null,
};
return tag != Variant.Types.TypeTag.Null;
}
private static Type ClrType(Variant.Types.TypeTag tag) => tag switch
{
Variant.Types.TypeTag.Boolean => typeof(bool),
Variant.Types.TypeTag.Sbyte => typeof(sbyte),
Variant.Types.TypeTag.Byte => typeof(byte),
Variant.Types.TypeTag.Int16 => typeof(short),
Variant.Types.TypeTag.Uint16 => typeof(ushort),
Variant.Types.TypeTag.Int32 => typeof(int),
Variant.Types.TypeTag.Uint32 => typeof(uint),
Variant.Types.TypeTag.Int64 => typeof(long),
Variant.Types.TypeTag.Uint64 => typeof(ulong),
Variant.Types.TypeTag.Float => typeof(float),
Variant.Types.TypeTag.Double => typeof(double),
Variant.Types.TypeTag.String => typeof(string),
Variant.Types.TypeTag.DatetimeUtc => typeof(DateTime),
_ => throw new ScriptWorkerProtocolException($"array element type {tag} is not supported"),
};
}