From 2557811e1fc826c79a58931d40adfa08db6e7605 Mon Sep 17 00:00:00 2001 From: Kouji Matsui Date: Wed, 17 Apr 2024 11:55:26 +0900 Subject: [PATCH 1/3] Updated GA script. --- .github/workflows/build.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 80d4544..c820040 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -64,10 +64,10 @@ jobs: #----------------------------------------------------------------------- # Deploy packages (develop) - - name: Deploy NuGet package (develop/ref1) - if: startsWith( github.ref, 'refs/tags/' ) - run: | - dotnet nuget push artifacts/DupeNukem.*.nupkg --source ref1 + #- name: Deploy NuGet package (develop/ref1) + # if: startsWith( github.ref, 'refs/tags/' ) + # run: | + # dotnet nuget push artifacts/DupeNukem.*.nupkg --source ref1 #----------------------------------------------------------------------- # Deploy packages (main) From b8486e2adae8a7670b8fe6e5d0f255d36c4788d0 Mon Sep 17 00:00:00 2001 From: Kouji Matsui Date: Wed, 17 Apr 2024 21:56:15 +0900 Subject: [PATCH 2/3] Added JsonToken types. --- .../{Internal => }/ConverterContext.cs | 51 +- DupeNukem.Core/Internal/JsonTokenConverter.cs | 49 ++ DupeNukem.Core/Internal/MethodDescriptors.cs | 24 +- .../Internal/SuspendingDescriptor.cs | 20 +- DupeNukem.Core/JsonToken.cs | 581 ++++++++++++++++++ DupeNukem.Core/Messenger.cs | 5 +- .../ViewModels/TestFragments.cs | 76 +++ 7 files changed, 772 insertions(+), 34 deletions(-) rename DupeNukem.Core/{Internal => }/ConverterContext.cs (64%) create mode 100644 DupeNukem.Core/Internal/JsonTokenConverter.cs create mode 100644 DupeNukem.Core/JsonToken.cs diff --git a/DupeNukem.Core/Internal/ConverterContext.cs b/DupeNukem.Core/ConverterContext.cs similarity index 64% rename from DupeNukem.Core/Internal/ConverterContext.cs rename to DupeNukem.Core/ConverterContext.cs index ebc7d10..577a4eb 100644 --- a/DupeNukem.Core/Internal/ConverterContext.cs +++ b/DupeNukem.Core/ConverterContext.cs @@ -10,12 +10,14 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.ComponentModel; using System.Diagnostics; using System.Threading; -namespace DupeNukem.Internal; +namespace DupeNukem; -internal static class ConverterContext +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class ConverterContext { // JsonSerializer cannot pass application-specific context information // to the Converter during serialization runs. @@ -68,27 +70,37 @@ public void Exit(IMessenger messenger) private static readonly ThreadLocal messengers = new(() => new MessengerContext()); - public static Messenger Current + internal static Messenger Current { get { AssertValidState(); - return (Messenger)messengers.Value!.Current; + + // If the cast fails, you need real `Messenger` that actually works. + // Perhaps you are using mocks in your unit tests. + // Messenger is necessary for successful DupeNukem serialization. + if (messengers.Value!.Current is not Messenger m) + { + throw new InvalidOperationException( + "DupeNukem: You need real `Messenger` instance."); + } + return m; } } [Conditional("DEBUG")] - public static void AssertValidState() => + internal static void AssertValidState() => Debug.Assert( messengers.Value!.Current is Messenger, "Invalid state: Not called correctly."); - public static void Enter(IMessenger messenger) => + internal static void Enter(IMessenger messenger) => messengers.Value!.Enter(messenger); - public static void Exit(IMessenger messenger) => + internal static void Exit(IMessenger messenger) => messengers.Value!.Exit(messenger); + [EditorBrowsable(EditorBrowsableState.Advanced)] public static void Run(IMessenger messenger, Action action) { messengers.Value!.Enter(messenger); @@ -102,6 +114,7 @@ public static void Run(IMessenger messenger, Action action) } } + [EditorBrowsable(EditorBrowsableState.Advanced)] public static T Run(IMessenger messenger, Func action) { messengers.Value!.Enter(messenger); @@ -114,4 +127,28 @@ public static T Run(IMessenger messenger, Func action) messengers.Value!.Exit(messenger); } } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static IDisposable Begin(IMessenger messenger) + { + messengers.Value!.Enter(messenger); + return new Disposer(messenger); + } + + private sealed class Disposer : IDisposable + { + private IMessenger? messenger; + + public Disposer(IMessenger messenger) => + this.messenger = messenger; + + public void Dispose() + { + if (this.messenger is { } messenger) + { + this.messenger = null!; + messengers.Value!.Exit(messenger); + } + } + } } diff --git a/DupeNukem.Core/Internal/JsonTokenConverter.cs b/DupeNukem.Core/Internal/JsonTokenConverter.cs new file mode 100644 index 0000000..4d9c7d9 --- /dev/null +++ b/DupeNukem.Core/Internal/JsonTokenConverter.cs @@ -0,0 +1,49 @@ +//////////////////////////////////////////////////////////////////////////// +// +// DupeNukem - WebView attachable full-duplex asynchronous interoperable +// independent messaging library between .NET and JavaScript. +// +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DupeNukem.Internal; + +internal sealed class JsonTokenConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) => + typeof(JsonToken).IsAssignableFrom(objectType); + + public override object? ReadJson( + JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + ConverterContext.AssertValidState(); + + if (serializer.Deserialize(reader) is { } token) + { + return JsonToken.FromJToken(ConverterContext.Current, token); + } + return null; + } + + public override void WriteJson( + JsonWriter writer, object? value, JsonSerializer serializer) + { + ConverterContext.AssertValidState(); + + if (value is JsonToken t) + { + serializer.Serialize(writer, t.token); + } + else + { + writer.WriteNull(); + } + } +} diff --git a/DupeNukem.Core/Internal/MethodDescriptors.cs b/DupeNukem.Core/Internal/MethodDescriptors.cs index 6afa1e0..334cf47 100644 --- a/DupeNukem.Core/Internal/MethodDescriptors.cs +++ b/DupeNukem.Core/Internal/MethodDescriptors.cs @@ -59,28 +59,8 @@ arg is { } a ? Activator.CreateInstance(type) : null; - private protected IDisposable BeginConverterContext() - { - ConverterContext.Enter(this.messenger); - return new Disposer(this.messenger); - } - - private sealed class Disposer : IDisposable - { - private IMessenger? messenger; - - public Disposer(IMessenger messenger) => - this.messenger = messenger; - - public void Dispose() - { - if (this.messenger is { } messenger) - { - this.messenger = null; - ConverterContext.Exit(messenger); - } - } - } + private protected IDisposable BeginConverterContext() => + ConverterContext.Begin(this.messenger); } /////////////////////////////////////////////////////////////////////////////// diff --git a/DupeNukem.Core/Internal/SuspendingDescriptor.cs b/DupeNukem.Core/Internal/SuspendingDescriptor.cs index ba968a5..bb10e21 100644 --- a/DupeNukem.Core/Internal/SuspendingDescriptor.cs +++ b/DupeNukem.Core/Internal/SuspendingDescriptor.cs @@ -48,13 +48,20 @@ public override void Cancel() => // SuspendingDescriptor with a return value type. internal sealed class SuspendingDescriptor : SuspendingDescriptor { + private readonly Messenger messenger; private readonly TaskCompletionSource tcs = new(); + public SuspendingDescriptor(Messenger messenger) => + this.messenger = messenger; + public Task Task => this.tcs.Task; public override void Resolve(JToken? result) => - this.tcs.TrySetResult(result is { } ? result.ToObject()! : default!); + this.tcs.TrySetResult(result is { } ? + ConverterContext.Run(this.messenger, () => + result.ToObject(this.messenger.Serializer)!) : + default!); public override void Reject(Exception ex) => this.tcs.TrySetException(ex); public override void Cancel() => @@ -64,17 +71,24 @@ public override void Cancel() => // SuspendingDescriptor with a return value type at runtime. internal sealed class DynamicSuspendingDescriptor : SuspendingDescriptor { + private readonly Messenger messenger; private readonly Type returnType; private readonly TaskCompletionSource tcs = new(); - public DynamicSuspendingDescriptor(Type returnType) => + public DynamicSuspendingDescriptor(Messenger messenger, Type returnType) + { + this.messenger = messenger; this.returnType = returnType; + } public Task Task => this.tcs.Task; public override void Resolve(JToken? result) => - this.tcs.TrySetResult(result is { } ? result.ToObject(this.returnType)! : default!); + this.tcs.TrySetResult(result is { } ? + ConverterContext.Run(this.messenger, () => + result.ToObject(this.returnType, this.messenger.Serializer)!) : + default!); public override void Reject(Exception ex) => this.tcs.TrySetException(ex); public override void Cancel() => diff --git a/DupeNukem.Core/JsonToken.cs b/DupeNukem.Core/JsonToken.cs new file mode 100644 index 0000000..843371a --- /dev/null +++ b/DupeNukem.Core/JsonToken.cs @@ -0,0 +1,581 @@ +//////////////////////////////////////////////////////////////////////////// +// +// DupeNukem - WebView attachable full-duplex asynchronous interoperable +// independent messaging library between .NET and JavaScript. +// +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; + +namespace DupeNukem; + +// For loose type Json serializing. +// Using these classes, closures and byte arrays can also be handled correctly. +// Usage is nearly same as JToken and other classes. + +/// +/// Safer represent JToken type. +/// +public abstract class JsonToken : + IEquatable +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal readonly Messenger? messenger; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal readonly JToken token; + + private protected JsonToken( + Messenger? messenger, JToken token) + { + this.messenger = messenger; + this.token = token; + } + + /// + /// Get related serializer. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + [EditorBrowsable(EditorBrowsableState.Advanced)] + [JsonIgnore] + public JsonSerializer? Serializer => + this.messenger?.Serializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + [EditorBrowsable(EditorBrowsableState.Advanced)] + [JsonIgnore] + public JToken Token => + this.token; + + [JsonIgnore] + public JTokenType JsonType => + this.token.Type; + + public bool Equals(JsonToken? rhs) => + rhs is { } r && + JToken.EqualityComparer.Equals(this.token, r.token); + + public override bool Equals(object? obj) => + this.Equals(obj as JsonToken); + + public override int GetHashCode() => + JToken.EqualityComparer.GetHashCode(this.token); + + public string ToString(Formatting formatting) => + this.Serializer is { } s ? + this.token.ToString(formatting, s.Converters.ToArray()) : + this.token.ToString(formatting); + + public override string ToString() => + this.ToString(Formatting.Indented); + + private protected IDisposable? Begin() => + this.messenger is { } m ? ConverterContext.Begin(m) : null; + + public object? ToObject(Type objectType) + { + using var _ = this.Begin(); + + return this.messenger != null ? + this.token.ToObject(objectType, this.messenger.Serializer) : + this.token.ToObject(objectType); + } + + public T ToObject() + { + using var _ = this.Begin(); + + return this.messenger != null ? + this.token.ToObject(this.messenger.Serializer)! : + this.token.ToObject()!; + } + + internal static JsonToken? FromJToken( + Messenger? messenger, JToken? token) => + token switch + { + null => null, + JObject obj => new JsonObject(messenger, obj), + JArray arr => new JsonArray(messenger, arr), + JValue v => new JsonValue(messenger, v), + _ => throw new ArgumentException(), + }; + + /// + /// Create JsonToken from an instance. + /// + /// Instance or null + /// JsonToken or null if success. + public static JsonToken? FromObject(object? value) => + value switch + { + null => null, + bool v => new JsonValue(null, new JValue(v)), + byte v => new JsonValue(null, new JValue(v)), + sbyte v => new JsonValue(null, new JValue(v)), + short v => new JsonValue(null, new JValue(v)), + ushort v => new JsonValue(null, new JValue(v)), + int v => new JsonValue(null, new JValue(v)), + uint v => new JsonValue(null, new JValue(v)), + long v => new JsonValue(null, new JValue(v)), + ulong v => new JsonValue(null, new JValue(v)), + float v => new JsonValue(null, new JValue(v)), + double v => new JsonValue(null, new JValue(v)), + decimal v => new JsonValue(null, new JValue(v)), + char v => new JsonValue(null, new JValue(v)), + string v => new JsonValue(null, new JValue(v)), + DateTime v => new JsonValue(null, new JValue(v)), + DateTimeOffset v => new JsonValue(null, new JValue(v)), + TimeSpan v => new JsonValue(null, new JValue(v)), + Guid v => new JsonValue(null, new JValue(v)), + Uri v => new JsonValue(null, new JValue(v)), + Array arr => new JsonArray(null, JArray.FromObject(arr)), + Enum v => new JsonValue(null, new JValue(v)), + _ => new JsonObject(null, JObject.FromObject(value)), + }; + + public static implicit operator JsonToken(bool value) => + FromObject(value)!; + public static implicit operator JsonToken(byte value) => + FromObject(value)!; + public static implicit operator JsonToken(sbyte value) => + FromObject(value)!; + public static implicit operator JsonToken(short value) => + FromObject(value)!; + public static implicit operator JsonToken(ushort value) => + FromObject(value)!; + public static implicit operator JsonToken(int value) => + FromObject(value)!; + public static implicit operator JsonToken(uint value) => + FromObject(value)!; + public static implicit operator JsonToken(long value) => + FromObject(value)!; + public static implicit operator JsonToken(ulong value) => + FromObject(value)!; + public static implicit operator JsonToken(float value) => + FromObject(value)!; + public static implicit operator JsonToken(double value) => + FromObject(value)!; + public static implicit operator JsonToken(decimal value) => + FromObject(value)!; + public static implicit operator JsonToken(char value) => + FromObject(value)!; + public static implicit operator JsonToken(string value) => + FromObject(value)!; + public static implicit operator JsonToken(DateTime value) => + FromObject(value)!; + public static implicit operator JsonToken(DateTimeOffset value) => + FromObject(value)!; + public static implicit operator JsonToken(TimeSpan value) => + FromObject(value)!; + public static implicit operator JsonToken(Guid value) => + FromObject(value)!; + public static implicit operator JsonToken(Uri value) => + FromObject(value)!; + public static implicit operator JsonToken(Array value) => + FromObject(value)!; + public static implicit operator JsonToken(Enum value) => + FromObject(value)!; +} + +/// +/// Safer represent JContainer type. +/// +public abstract class JsonContainer : + JsonToken, IEnumerable +{ + private protected JsonContainer( + Messenger? messenger, JContainer token) : + base(messenger, token) + { + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private new JContainer token => + (JContainer)base.token; + + [JsonIgnore] + public int Count => + this.token.Count; + + private protected abstract IEnumerator OnGetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + this.OnGetEnumerator(); + + /// + /// Create JsonContainer derived instance from an instance. + /// + /// Instance or null + /// JsonContainer or null if success. + public static new JsonContainer? FromObject(object? value) => + value switch + { + null => null, + bool _ => throw new ArgumentException(), + byte _ => throw new ArgumentException(), + sbyte _ => throw new ArgumentException(), + short _ => throw new ArgumentException(), + ushort _ => throw new ArgumentException(), + int _ => throw new ArgumentException(), + uint _ => throw new ArgumentException(), + long _ => throw new ArgumentException(), + ulong _ => throw new ArgumentException(), + float _ => throw new ArgumentException(), + double _ => throw new ArgumentException(), + decimal _ => throw new ArgumentException(), + char _ => throw new ArgumentException(), + string _ => throw new ArgumentException(), + DateTime _ => throw new ArgumentException(), + DateTimeOffset _ => throw new ArgumentException(), + TimeSpan _ => throw new ArgumentException(), + Guid _ => throw new ArgumentException(), + Uri _ => throw new ArgumentException(), + Array arr => new JsonArray(null, JArray.FromObject(arr)), + Enum _ => throw new ArgumentException(), + _ => new JsonObject(null, JObject.FromObject(value)), + }; + + public static implicit operator JsonContainer(Array value) => + FromObject(value)!; +} + +[DebuggerDisplay("{Name} : {Value}")] +internal readonly struct JsonPropertyItem +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string Name { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public JsonToken? Value { get; } + + public JsonPropertyItem(string name, JsonToken? value) + { + this.Name = name; + this.Value = value; + } +} + +internal sealed class JsonObjectDebuggerTypeProxy +{ + private JsonObject jo; + + internal JsonObjectDebuggerTypeProxy(JsonObject jo) => + this.jo = jo; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + internal JsonPropertyItem[] Items => + this.jo.Select(kv => new JsonPropertyItem(kv.Key, kv.Value)).ToArray(); +} + +/// +/// Safer represent JObject type. +/// +[DebuggerDisplay("JsonObject: Count = {Count}")] +[DebuggerTypeProxy(typeof(JsonObjectDebuggerTypeProxy))] +public sealed class JsonObject : + JsonContainer, IEnumerable>, IEnumerable +#if !NET35 && !NET40 + , IReadOnlyDictionary +#endif +{ + internal JsonObject( + Messenger? messenger, JObject token) : + base(messenger, token) + { + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private new JObject token => + (JObject)base.token; + + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + [JsonIgnore] + public IEnumerable Keys + { + get + { + using var _ = base.Begin(); + + foreach (var p in this.token.Children().OfType()) + { + yield return p.Name; + } + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + [JsonIgnore] + public IEnumerable Values + { + get + { + using var _ = base.Begin(); + + foreach (var p in this.token.Children().OfType()) + { + yield return FromJToken(this.messenger, p.Value)!; + } + } + } + + [JsonIgnore] + public JsonToken this[string propertyName] + { + get + { + using var _ = base.Begin(); + + return this.token.TryGetValue(propertyName, out var t) ? + FromJToken(this.messenger!, t)! : + throw new KeyNotFoundException(propertyName); + } + } + + public bool ContainsKey(string key) => + this.token.ContainsKey(key); + + public bool TryGetValue(string key, out JsonToken? value) + { + using var _ = base.Begin(); + + if (this.token.TryGetValue(key, out var t)) + { + value = FromJToken(this.messenger, t); + return true; + } + else + { + value = null!; + return false; + } + } + + public IEnumerator> GetEnumerator() + { + using var _ = base.Begin(); + + foreach (var p in this.token.Children().OfType()) + { + yield return new(p.Name, FromJToken(this.messenger, p.Value)); + } + } + + private protected override IEnumerator OnGetEnumerator() => + this.GetEnumerator(); + + /// + /// Create JsonObject derived instance from an instance. + /// + /// Instance or null + /// JsonObject or null if success. + public static new JsonObject? FromObject(object? value) => + value switch + { + null => null, + bool _ => throw new ArgumentException(), + byte _ => throw new ArgumentException(), + sbyte _ => throw new ArgumentException(), + short _ => throw new ArgumentException(), + ushort _ => throw new ArgumentException(), + int _ => throw new ArgumentException(), + uint _ => throw new ArgumentException(), + long _ => throw new ArgumentException(), + ulong _ => throw new ArgumentException(), + float _ => throw new ArgumentException(), + double _ => throw new ArgumentException(), + decimal _ => throw new ArgumentException(), + char _ => throw new ArgumentException(), + string _ => throw new ArgumentException(), + DateTime _ => throw new ArgumentException(), + DateTimeOffset _ => throw new ArgumentException(), + TimeSpan _ => throw new ArgumentException(), + Guid _ => throw new ArgumentException(), + Uri _ => throw new ArgumentException(), + Array _ => throw new ArgumentException(), + Enum _ => throw new ArgumentException(), + _ => new JsonObject(null, new JObject(value)), + }; +} + +internal sealed class JsonArrayDebuggerTypeProxy +{ + private JsonArray ja; + + internal JsonArrayDebuggerTypeProxy(JsonArray ja) => + this.ja = ja; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + internal JsonToken?[] Items => + this.ja.ToArray(); +} + +/// +/// Safer represent JArray type. +/// +[DebuggerDisplay("JsonArray: Count = {Count}")] +[DebuggerTypeProxy(typeof(JsonArrayDebuggerTypeProxy))] +public sealed class JsonArray : + JsonContainer, IEnumerable, IEnumerable +#if !NET35 && !NET40 + , IReadOnlyList +#endif +{ + internal JsonArray( + Messenger? messenger, JArray token) : + base(messenger, token) + { + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private new JArray token => + (JArray)base.token; + + [JsonIgnore] + public JsonToken? this[int index] + { + get + { + using var _ = base.Begin(); + + return FromJToken(this.messenger, this.token[index]); + } + } + + public IEnumerator GetEnumerator() + { + using var _ = base.Begin(); + + foreach (var t in this.token.Children()) + { + yield return FromJToken(this.messenger, t); + } + } + + private protected override IEnumerator OnGetEnumerator() => + this.GetEnumerator(); + + public static new JsonArray? FromObject(object? value) => + value switch + { + null => null, + Array arr => new JsonArray(null, new JArray(arr)), + _ => throw new ArgumentException(), + }; + + public static implicit operator JsonArray(Array value) => + FromObject(value)!; +} + +/// +/// Safer represent JValue type. +/// +[DebuggerDisplay("{DisplayString}")] +public sealed class JsonValue : + JsonToken +{ + internal JsonValue( + Messenger? messenger, JValue token) : + base(messenger, token) + { + } + + public object? Value => + ((JValue)base.token).Value; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DisplayString => + this.Value switch + { + null => null, + string s => $"\"{s.Replace("\"", "\\\"")}\"", + bool v => v.ToString(), + var v => $"{v.GetType().Name}: {v}", + } ?? "(null)"; + + /// + /// Create JsonValue derived instance from an instance. + /// + /// Instance or null + /// JsonValue or null if success. + public static new JsonValue? FromObject(object? value) => + value switch + { + null => null, + bool v => new JsonValue(null, new JValue(v)), + byte v => new JsonValue(null, new JValue(v)), + sbyte v => new JsonValue(null, new JValue(v)), + short v => new JsonValue(null, new JValue(v)), + ushort v => new JsonValue(null, new JValue(v)), + int v => new JsonValue(null, new JValue(v)), + uint v => new JsonValue(null, new JValue(v)), + long v => new JsonValue(null, new JValue(v)), + ulong v => new JsonValue(null, new JValue(v)), + float v => new JsonValue(null, new JValue(v)), + double v => new JsonValue(null, new JValue(v)), + decimal v => new JsonValue(null, new JValue(v)), + char v => new JsonValue(null, new JValue(v)), + string v => new JsonValue(null, new JValue(v)), + DateTime v => new JsonValue(null, new JValue(v)), + DateTimeOffset v => new JsonValue(null, new JValue(v)), + TimeSpan v => new JsonValue(null, new JValue(v)), + Guid v => new JsonValue(null, new JValue(v)), + Uri v => new JsonValue(null, new JValue(v)), + Enum v => new JsonValue(null, new JValue(v)), + _ => throw new ArgumentException(), + }; + + public static implicit operator JsonValue(bool value) => + FromObject(value)!; + public static implicit operator JsonValue(byte value) => + FromObject(value)!; + public static implicit operator JsonValue(sbyte value) => + FromObject(value)!; + public static implicit operator JsonValue(short value) => + FromObject(value)!; + public static implicit operator JsonValue(ushort value) => + FromObject(value)!; + public static implicit operator JsonValue(int value) => + FromObject(value)!; + public static implicit operator JsonValue(uint value) => + FromObject(value)!; + public static implicit operator JsonValue(long value) => + FromObject(value)!; + public static implicit operator JsonValue(ulong value) => + FromObject(value)!; + public static implicit operator JsonValue(float value) => + FromObject(value)!; + public static implicit operator JsonValue(double value) => + FromObject(value)!; + public static implicit operator JsonValue(decimal value) => + FromObject(value)!; + public static implicit operator JsonValue(char value) => + FromObject(value)!; + public static implicit operator JsonValue(string value) => + FromObject(value)!; + public static implicit operator JsonValue(DateTime value) => + FromObject(value)!; + public static implicit operator JsonValue(DateTimeOffset value) => + FromObject(value)!; + public static implicit operator JsonValue(TimeSpan value) => + FromObject(value)!; + public static implicit operator JsonValue(Guid value) => + FromObject(value)!; + public static implicit operator JsonValue(Uri value) => + FromObject(value)!; + public static implicit operator JsonValue(Array value) => + FromObject(value)!; + public static implicit operator JsonValue(Enum value) => + FromObject(value)!; +} diff --git a/DupeNukem.Core/Messenger.cs b/DupeNukem.Core/Messenger.cs index 34626bc..e46f74c 100644 --- a/DupeNukem.Core/Messenger.cs +++ b/DupeNukem.Core/Messenger.cs @@ -87,6 +87,7 @@ public static JsonSerializer GetDefaultJsonSerializer() serializer.Converters.Add(new ByteArrayConverter()); serializer.Converters.Add(new CancellationTokenConverter()); serializer.Converters.Add(new ClosureConverter()); + serializer.Converters.Add(new JsonTokenConverter()); return serializer; } @@ -360,7 +361,7 @@ public Task InvokePeerMethodAsync( { ct.ThrowIfCancellationRequested(); - var descriptor = new SuspendingDescriptor(); + var descriptor = new SuspendingDescriptor(this); this.SendInvokeMessageToPeer(descriptor, ct, methodName, args); return descriptor.Task; @@ -371,7 +372,7 @@ public Task InvokePeerMethodAsync( { ct.ThrowIfCancellationRequested(); - var descriptor = new DynamicSuspendingDescriptor(returnType); + var descriptor = new DynamicSuspendingDescriptor(this, returnType); this.SendInvokeMessageToPeer(descriptor, ct, methodName, args); return descriptor.Task; diff --git a/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs b/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs index 9d87cac..1175bad 100644 --- a/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs +++ b/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs @@ -29,9 +29,31 @@ namespace DupeNukem.ViewModels; partial class ContentPageViewModel #else namespace DupeNukem.ViewModels; + partial class MainWindowViewModel #endif { + public sealed class CustomType + { + public int Value1 = 0; + public string? Value2 = null; + public CustomType? Value3 = null; + public JsonToken? Value4 = null; + + public override bool Equals(object? rhs) => + rhs is CustomType r && + this.Value1.Equals(r.Value1) && + (this.Value2 == r.Value2) && + (this.Value3?.Equals(r.Value3) ?? r.Value3 == null) && + (this.Value4?.Equals(r.Value4) ?? r.Value4 == null); + + public override int GetHashCode() => + this.Value1.GetHashCode() ^ + (this.Value2?.GetHashCode() ?? 0) ^ + (this.Value3?.GetHashCode() ?? 0) ^ + (this.Value4?.GetHashCode() ?? 0); + } + private void RegisterTestObjects(WebViewMessenger messenger) { // ================================================ @@ -82,6 +104,9 @@ private void RegisterTestObjects(WebViewMessenger messenger) messenger.RegisterFunc>>( "callback2", async (a, b, cb) => { var r = await cb(a, b, default); return r; }); + messenger.RegisterFunc( + "customType", + async ct => { await Task.Delay(100); return ct; }); } ///////////////////////////////////////////////////////// @@ -153,6 +178,53 @@ private static void HookWithMessengerTestCode(WebViewMessenger messenger) Trace.WriteLine("PASSED: Unknown function invoking [unknown]"); } + var result_custonType1 = await messenger.InvokePeerMethodAsync( + "js_customType", + new CustomType + { + Value1 = 123, + Value2 = "ABC", + Value3 = new CustomType + { + Value1 = 456, + Value2 = "DEF", + Value4 = 111, + }, + Value4 = JsonToken.FromObject(new CustomType + { + Value1 = 789, + Value2 = "GHI", + Value4 = JsonToken.FromObject(new CustomType + { + Value1 = 999, + Value2 = "XXX", + Value4 = 222, + }), + }), + }); + Assert(new CustomType + { + Value1 = 123, + Value2 = "ABC", + Value3 = new CustomType + { + Value1 = 456, + Value2 = "DEF", + Value4 = 111, + }, + Value4 = JsonToken.FromObject(new CustomType + { + Value1 = 789, + Value2 = "GHI", + Value4 = JsonToken.FromObject(new CustomType + { + Value1 = 999, + Value2 = "XXX", + Value4 = 222, + }), + }), + }, result_custonType1, "js_customType"); + ///////////////////////////////////////////////////////// // Test JavaScript --> .NET methods with callback @@ -204,6 +276,7 @@ private static void AddJavaScriptTestCode(StringBuilder script) script.AppendLine("async function js_callback3(a, b, cb) { return await cb(a, b, new AbortController().signal); }"); script.AppendLine("async function js_arrayBuffer1(arr) { console.log('js_arrayBuffer1(' + arr + ')'); return arr; }"); script.AppendLine("async function js_arrayBuffer2(arr) { console.log('js_arrayBuffer2(' + arr + ')'); return new Uint8Array(arr); }"); + script.AppendLine("async function js_customType(ct) { console.log('js_customType(' + ct + ')'); return ct; }"); ///////////////////////////////////////////////////////// // Invoke JavaScript --> .NET methods: @@ -247,6 +320,9 @@ private static void AddJavaScriptTestCode(StringBuilder script) script.AppendLine(" const result_callback2 = await invokeHostMethod('callback2', 1, 2, async (a, b, ct) => a + b);"); script.AppendLine(" assert(3, result_callback2, 'callback2');"); + script.AppendLine(" const result_customType1 = await invokeHostMethod('customType', { value1:123, value2: 'ABC', value3: { value1: 456, value2: 'DEF', value4: 111 }, value4: { value1: 789, value2: 'GHI', value4: { value1: 999, value2: 'XXX', value4: 222 } } });"); + script.AppendLine(" assert(JSON.stringify({ value1:123, value2: 'ABC', value3: { value1: 456, value2: 'DEF', value3: null, value4: 111 }, value4: { value1: 789, value2: 'GHI', value4: { value1: 999, value2: 'XXX', value4: 222 } } }), JSON.stringify(result_customType1), 'customType1');"); + // Unknown method with `invokeHostMethod()`. script.AppendLine(" try {"); script.AppendLine(" await invokeHostMethod('unknown', 12, 34, 56);"); From fdeacea89a0fdd0f7f1d625d53d45211bb53541e Mon Sep 17 00:00:00 2001 From: Kouji Matsui Date: Thu, 18 Apr 2024 00:05:33 +0900 Subject: [PATCH 3/3] Updated readme. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 287f3b5..71602dc 100644 --- a/README.md +++ b/README.md @@ -655,6 +655,10 @@ Apache-v2. ## History +* 0.28.0: + * Supported `JsonToken` type sets. + If you want to handle untyped JSON using the `JToken` type, + use it instead and the serialization will be correct. * 0.27.0: * Supported serialization for JavaScript `ArrayBuffer`, `Uint8Array` and `Uint8ClampedArray` from/to .NET `byte[]`. * Refactored object referencing handlers (In `AbortSignal` and function closures.)