diff --git a/Neuroglia Framework.sln b/Neuroglia Framework.sln index 9f5a596b3..6363a9234 100644 --- a/Neuroglia Framework.sln +++ b/Neuroglia Framework.sln @@ -98,10 +98,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neuroglia.Mediation.Abstrac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neuroglia.Integration", "src\Neuroglia.Integration\Neuroglia.Integration.csproj", "{B9DF1C37-F41F-4AC8-8BB0-19AD9EB978D7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neuroglia.JsonSchema.Generation", "src\Neuroglia.JsonSchema.Generation\Neuroglia.JsonSchema.Generation.csproj", "{30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neuroglia.Measurements.Sdk.TypeScript", "src\Neuroglia.Measurements.Sdk.TypeScript\Neuroglia.Measurements.Sdk.TypeScript.csproj", "{4BA3F773-5C01-4064-9441-EED11C3264EC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neuroglia.Data.Schemas.Json", "src\Neuroglia.Data.Schemas.Json\Neuroglia.Data.Schemas.Json.csproj", "{A555095D-AA84-4D67-BF84-A0038EC783BD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -276,14 +276,14 @@ Global {B9DF1C37-F41F-4AC8-8BB0-19AD9EB978D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {B9DF1C37-F41F-4AC8-8BB0-19AD9EB978D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9DF1C37-F41F-4AC8-8BB0-19AD9EB978D7}.Release|Any CPU.Build.0 = Release|Any CPU - {30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93}.Release|Any CPU.Build.0 = Release|Any CPU {4BA3F773-5C01-4064-9441-EED11C3264EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4BA3F773-5C01-4064-9441-EED11C3264EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BA3F773-5C01-4064-9441-EED11C3264EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {4BA3F773-5C01-4064-9441-EED11C3264EC}.Release|Any CPU.Build.0 = Release|Any CPU + {A555095D-AA84-4D67-BF84-A0038EC783BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A555095D-AA84-4D67-BF84-A0038EC783BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A555095D-AA84-4D67-BF84-A0038EC783BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A555095D-AA84-4D67-BF84-A0038EC783BD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -331,8 +331,8 @@ Global {63922ABE-916D-4A4D-A3E1-8162BEED060E} = {AFFADE1D-C17D-4ED8-8407-BFE108714262} {23AEEAC4-03C7-4E6F-AB74-26588908AAB9} = {BDED6037-3D6E-4781-8C58-2EB7D0E53CEA} {B9DF1C37-F41F-4AC8-8BB0-19AD9EB978D7} = {BDED6037-3D6E-4781-8C58-2EB7D0E53CEA} - {30EF0A31-DD4D-4A4C-A383-8AF88DE8FD93} = {BDED6037-3D6E-4781-8C58-2EB7D0E53CEA} {4BA3F773-5C01-4064-9441-EED11C3264EC} = {BDED6037-3D6E-4781-8C58-2EB7D0E53CEA} + {A555095D-AA84-4D67-BF84-A0038EC783BD} = {BDED6037-3D6E-4781-8C58-2EB7D0E53CEA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B390984D-3A9D-4F6D-A73B-E93CCEB586E4} diff --git a/src/Neuroglia.Core/Utilities/XmlDocumentationHelper.cs b/src/Neuroglia.Core/Utilities/XmlDocumentationHelper.cs index 220637f17..92cdbca0e 100644 --- a/src/Neuroglia.Core/Utilities/XmlDocumentationHelper.cs +++ b/src/Neuroglia.Core/Utilities/XmlDocumentationHelper.cs @@ -26,14 +26,14 @@ namespace Neuroglia; public static partial class XmlDocumentationHelper { - private static ConcurrentDictionary LoadedAssemblyXmlDocumentation = new(); + private static ConcurrentDictionary LoadedAssemblyXmlDocumentation = []; /// /// Gets the specified type's XML code documentation /// /// The type to get the XML code documentation for /// The XML code documentation of the specified type - public static string DocumentationOf(Type type) + public static string? DocumentationOf(Type type) { ArgumentNullException.ThrowIfNull(type); @@ -182,7 +182,7 @@ partial class AssemblyXmlDocumentationContainer AssemblyXmlDocumentationContainer() { } - public Dictionary XmlDocumentation { get; } = new(); + public Dictionary XmlDocumentation { get; } = []; public string? GetDocumentationFor(Type type) { diff --git a/src/Neuroglia.JsonSchema.Generation/JsonSchemaGeneratorConfiguration.cs b/src/Neuroglia.Data.Schemas.Json/JsonSchemaGeneratorConfiguration.cs similarity index 97% rename from src/Neuroglia.JsonSchema.Generation/JsonSchemaGeneratorConfiguration.cs rename to src/Neuroglia.Data.Schemas.Json/JsonSchemaGeneratorConfiguration.cs index 084bae194..6b4dcb724 100644 --- a/src/Neuroglia.JsonSchema.Generation/JsonSchemaGeneratorConfiguration.cs +++ b/src/Neuroglia.Data.Schemas.Json/JsonSchemaGeneratorConfiguration.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Neuroglia.Json.Schema.Generation; +namespace Neuroglia.Data.Schemas.Json; /// /// Exposes methods to handle s diff --git a/src/Neuroglia.JsonSchema.Generation/Neuroglia.JsonSchema.Generation.csproj b/src/Neuroglia.Data.Schemas.Json/Neuroglia.Data.Schemas.Json.csproj similarity index 77% rename from src/Neuroglia.JsonSchema.Generation/Neuroglia.JsonSchema.Generation.csproj rename to src/Neuroglia.Data.Schemas.Json/Neuroglia.Data.Schemas.Json.csproj index d97333ac6..f002b380c 100644 --- a/src/Neuroglia.JsonSchema.Generation/Neuroglia.JsonSchema.Generation.csproj +++ b/src/Neuroglia.Data.Schemas.Json/Neuroglia.Data.Schemas.Json.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,7 +10,7 @@ Neuroglia SRL https://github.com/neuroglia-io/framework git - neuroglia framework json json-schema schema generation + neuroglia framework schema json json-schema 4.7.1 en Apache-2.0 @@ -32,4 +32,9 @@ + + + + + diff --git a/src/Neuroglia.JsonSchema.Generation/DateTimeOffsetSchemaGenerator.cs b/src/Neuroglia.Data.Schemas.Json/Services/DateTimeOffsetSchemaGenerator.cs similarity index 96% rename from src/Neuroglia.JsonSchema.Generation/DateTimeOffsetSchemaGenerator.cs rename to src/Neuroglia.Data.Schemas.Json/Services/DateTimeOffsetSchemaGenerator.cs index 21d58585a..cdfadf6c0 100644 --- a/src/Neuroglia.JsonSchema.Generation/DateTimeOffsetSchemaGenerator.cs +++ b/src/Neuroglia.Data.Schemas.Json/Services/DateTimeOffsetSchemaGenerator.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Neuroglia.Json.Schema.Generation; +namespace Neuroglia.Data.Schemas.Json; /// /// Represents the used to handle s diff --git a/src/Neuroglia.Data.Schemas.Json/Services/Interfaces/IJsonSchemaResolver.cs b/src/Neuroglia.Data.Schemas.Json/Services/Interfaces/IJsonSchemaResolver.cs new file mode 100644 index 000000000..d86e2e6a1 --- /dev/null +++ b/src/Neuroglia.Data.Schemas.Json/Services/Interfaces/IJsonSchemaResolver.cs @@ -0,0 +1,30 @@ +// Copyright © 2021-Present Neuroglia SRL. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Neuroglia.Data.Schemas.Json; + +/// +/// Defines the fundamentals of a service used to resolve s +/// +public interface IJsonSchemaResolver +{ + + /// + /// Resolves the $refs of the specified + /// + /// The to resolve + /// A + /// The resolved + Task ResolveSchemaAsync(JsonSchema schema, CancellationToken cancellationToken = default); + +} diff --git a/src/Neuroglia.Data.Schemas.Json/Services/JsonSchemaResolver.cs b/src/Neuroglia.Data.Schemas.Json/Services/JsonSchemaResolver.cs new file mode 100644 index 000000000..7ee257033 --- /dev/null +++ b/src/Neuroglia.Data.Schemas.Json/Services/JsonSchemaResolver.cs @@ -0,0 +1,175 @@ +// Copyright © 2021-Present Neuroglia SRL. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using JsonCons.Utilities; +using Neuroglia.Serialization; +using System.Text.Json; +using System.Text.Json.Nodes; +using JsonPointer = Json.Pointer.JsonPointer; + +namespace Neuroglia.Data.Schemas.Json; + +/// +/// Represents the default implementation of the interface +/// +/// The service used to serialize/deserialize objects to/from JSON +/// The service used to perform HTTP requests +public class JsonSchemaResolver(IJsonSerializer serializer, HttpClient httpClient) + : IJsonSchemaResolver +{ + + /// + /// Gets the service used to serialize/deserialize objects to/from JSON + /// + protected IJsonSerializer Serializer { get; } = serializer; + + /// + /// Gets the service used to perform HTTP requests + /// + protected HttpClient HttpClient { get; } = httpClient; + + /// + public virtual async Task ResolveSchemaAsync(JsonSchema schema, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(schema); + + var jsonElement = this.Serializer.SerializeToElement(schema)!; + jsonElement = await this.ResolveSchemaAsync(jsonElement.Value, jsonElement, cancellationToken).ConfigureAwait(false); + var json = this.Serializer.SerializeToText(jsonElement)!; + return JsonSchema.FromText(json); + } + + /// + /// Resolves the specified + /// + /// The representation of the to resolve + /// The representation of the root , if any, of the to resolve + /// A + /// The representation of the to resolve + protected virtual async Task ResolveSchemaAsync(JsonElement schema, JsonElement? rootSchema = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(schema); + rootSchema ??= schema; + + switch (schema.ValueKind) + { + case JsonValueKind.Array: + var schemaArray = new JsonArray(); + foreach (var item in schema.EnumerateArray()) + { + schemaArray.Add(await this.ResolveSchemaAsync(item, rootSchema, cancellationToken).ConfigureAwait(false)); + } + return this.Serializer.SerializeToElement(schemaArray)!.Value; + case JsonValueKind.Object: + var refSchemas = await this.ResolveReferencedSchemasAsync(schema, rootSchema.Value, cancellationToken).ConfigureAwait(false); + var mergedSchema = this.RemoveReferenceProperties(schema); + foreach (var refSchema in refSchemas) + { + mergedSchema = JsonMergePatch.ApplyMergePatch(mergedSchema, refSchema).RootElement; + } + var mergedSchemaNode = this.Serializer.SerializeToNode(mergedSchema)!.AsObject()!; + foreach (var property in mergedSchema.EnumerateObject()) + { + mergedSchemaNode[property.Name] = this.Serializer.SerializeToNode(await this.ResolveSchemaAsync(property.Value, rootSchema, cancellationToken).ConfigureAwait(false)); + } + return this.Serializer.SerializeToElement(mergedSchemaNode)!.Value; + default: return schema; + } + } + + /// + /// Resolves the references specified + /// + /// The representation of the to resolve + /// The representation of the root , if any, of the to resolve + /// A + /// The representation of the to resolve + protected virtual async Task> ResolveReferencedSchemasAsync(JsonElement schema, JsonElement rootSchema, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(schema); + ArgumentNullException.ThrowIfNull(rootSchema); + + if (schema.ValueKind != JsonValueKind.Object) return Array.Empty(); + + var refSchemas = new List(); + var keywords = schema.EnumerateObject(); + var refKeyword = keywords.FirstOrDefault(k => k.NameEquals(RefKeyword.Name)); + var allOfKeyword = keywords.FirstOrDefault(k => k.NameEquals(AllOfKeyword.Name)); + + if (refKeyword.Value.ValueKind == JsonValueKind.String) + { + var reference = refKeyword.Value.Deserialize()!; + var refSchema = await this.ResolveReferencedSchemaAsync(new Uri(reference, UriKind.RelativeOrAbsolute), rootSchema, cancellationToken).ConfigureAwait(false); + if (refSchema.HasValue) refSchemas.Add(refSchema); + } + + if (allOfKeyword.Value.ValueKind == JsonValueKind.Array) + { + foreach (var refSchema in allOfKeyword.Value.EnumerateArray()) + { + refSchemas.Add(await this.ResolveSchemaAsync(refSchema, null, cancellationToken).ConfigureAwait(false)); + } + } + + return refSchemas; + } + + /// + /// Resolves the specified + /// + /// The of the to resolve + /// The representation of the root , if any, of the to resolve + /// A + /// The representation of the to resolve + protected virtual async Task ResolveReferencedSchemaAsync(Uri uri, JsonElement rootSchema, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(uri); + ArgumentNullException.ThrowIfNull(rootSchema); + var useRootSchema = true; + + JsonElement? schema; + if (uri.IsAbsoluteUri) + { + var schemaJson = await this.HttpClient.GetStringAsync(uri, cancellationToken).ConfigureAwait(false); + schema = this.Serializer.SerializeToElement(schemaJson); + useRootSchema = false; + } + else + { + if (!JsonPointer.TryParse(uri.OriginalString, out var jsonPointer) || jsonPointer == null) throw new NullReferenceException($"Failed to find the JSON schema at '{uri}'"); + schema = jsonPointer.Evaluate(rootSchema); + } + + if (schema == null) return null; + else return await this.ResolveSchemaAsync(schema.Value, useRootSchema ? rootSchema : null, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes references from the specified + /// + /// The representation of the to remove references from + /// The purged out of references + protected virtual JsonElement RemoveReferenceProperties(JsonElement schema) + { + ArgumentNullException.ThrowIfNull(schema); + + var node = this.Serializer.SerializeToNode(schema)?.AsObject(); + if (node == null) return schema; + + node.Remove(RefKeyword.Name); + node.Remove(AllOfKeyword.Name); + + return this.Serializer.SerializeToElement(node)!.Value; + } + +} \ No newline at end of file diff --git a/src/Neuroglia.JsonSchema.Generation/Usings.cs b/src/Neuroglia.Data.Schemas.Json/Usings.cs similarity index 100% rename from src/Neuroglia.JsonSchema.Generation/Usings.cs rename to src/Neuroglia.Data.Schemas.Json/Usings.cs diff --git a/src/Neuroglia.Integration/DataTransferObject.cs b/src/Neuroglia.Integration/DataTransferObject.cs index 438cebd85..23492c457 100644 --- a/src/Neuroglia.Integration/DataTransferObject.cs +++ b/src/Neuroglia.Integration/DataTransferObject.cs @@ -11,11 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Runtime.Serialization; + namespace Neuroglia; /// /// Represents the base class of all Data Transfer Objects (DTOs) /// +[DataContract] public abstract record DataTransferObject : IDataTransferObject { diff --git a/src/Neuroglia.Integration/IntegrationEvent.cs b/src/Neuroglia.Integration/IntegrationEvent.cs index 80e6c9207..d48c4cfe8 100644 --- a/src/Neuroglia.Integration/IntegrationEvent.cs +++ b/src/Neuroglia.Integration/IntegrationEvent.cs @@ -11,42 +11,36 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Runtime.Serialization; + namespace Neuroglia; /// /// Represents the base class for all integration events /// -public abstract record IntegrationEvent +[DataContract] +public abstract record IntegrationEvent : DataTransferObject, IIntegrationEvent { /// /// Gets/sets the date and time at which the integration event has been produced /// + [DataMember] public virtual DateTimeOffset CreatedAt { get; set; } /// /// Gets/sets the id of the aggregate, if any, that has produced the event /// - public virtual object? AggregateId { get; set; } + [DataMember] + public virtual TKey? AggregateId { get; set; } /// /// Gets/sets the version of the aggregate, if any, that has produced the event /// + [DataMember] public ulong? AggregateVersion { get; set; } -} - -/// -/// Represents the base class for all integration events -/// -public abstract record IntegrationEvent - : IntegrationEvent -{ - - /// - /// Gets/sets the id of the aggregate, if any, that has produced the event - /// - public virtual new TKey? AggregateId { get; set; } + object? IIntegrationEvent.AggregateId => this.AggregateId; } diff --git a/src/Neuroglia.Serialization.Abstractions/Json/JsonSerializer.cs b/src/Neuroglia.Serialization.Abstractions/Json/JsonSerializer.cs index 286110f2f..272a614ce 100644 --- a/src/Neuroglia.Serialization.Abstractions/Json/JsonSerializer.cs +++ b/src/Neuroglia.Serialization.Abstractions/Json/JsonSerializer.cs @@ -28,7 +28,7 @@ namespace Neuroglia.Serialization.Json; /// /// The current public class JsonSerializer(IOptions options) - : IJsonSerializer, IAsyncSerializer + : IJsonSerializer, IAsyncSerializer { /// @@ -84,41 +84,15 @@ public static JsonSerializer Default /// public virtual string SerializeToText(object? value, Type? type = null) => System.Text.Json.JsonSerializer.Serialize(value, type ?? value?.GetType()!, this.Options); - /// - /// Serializes the specified object into a new - /// - /// The type of object to serialize - /// The object to serialize - /// A new /// public virtual JsonNode? SerializeToNode(T graph) => System.Text.Json.JsonSerializer.SerializeToNode(graph, this.Options); - /// - /// Serializes the specified object into a new - /// - /// The type of object to serialize - /// The object to serialize - /// A new /// public virtual JsonElement? SerializeToElement(T graph) => System.Text.Json.JsonSerializer.SerializeToElement(graph, this.Options); - /// - /// Serializes the specified object into a new - /// - /// The type of object to serialize - /// The object to serialize - /// A new /// public virtual JsonDocument? SerializeToDocument(T graph) => System.Text.Json.JsonSerializer.SerializeToDocument(graph, this.Options); - /// - /// Serializes an object to the specified - /// - /// The type of the object to serialize - /// The to serialize the object to - /// The object to serialize - /// A - /// A new awaitable /// public virtual Task SerializeAsync(Stream stream, T graph, CancellationToken cancellationToken = default) => System.Text.Json.JsonSerializer.SerializeAsync(stream, graph, this.Options, cancellationToken); @@ -131,44 +105,19 @@ public static JsonSerializer Default /// public virtual object? Deserialize(Stream stream, Type type) => System.Text.Json.JsonSerializer.Deserialize(stream, type, this.Options); - /// - /// Deserializes the specified - /// - /// The to deserialize - /// The type to deserialize the specified into - /// The deserialized value + /// public virtual object? Deserialize(JsonElement element, Type type) => System.Text.Json.JsonSerializer.Deserialize(element, type, this.Options); - /// - /// Deserializes the specified - /// - /// The type to deserialize the specified into - /// The to deserialize - /// The deserialized value + /// public virtual T? Deserialize(JsonElement element) => System.Text.Json.JsonSerializer.Deserialize(element, this.Options); - /// - /// Deserializes the specified JSON input - /// - /// The type to deserialize the specified JSON into - /// The JSON input to deserialize - /// The deserialized value + /// public virtual T? Deserialize(string json) => System.Text.Json.JsonSerializer.Deserialize(json, this.Options); - /// - /// Deserializes the specified - /// - /// The to deserialize - /// The type to deserialize the specified into - /// The deserialized value + /// public virtual object? Deserialize(JsonNode node, Type type) => System.Text.Json.JsonSerializer.Deserialize(node, type, this.Options); - /// - /// Deserializes the specified - /// - /// The type to deserialize the specified into - /// The to deserialize - /// The deserialized value + /// public virtual T? Deserialize(JsonNode node) => System.Text.Json.JsonSerializer.Deserialize(node, this.Options); /// @@ -190,13 +139,7 @@ public static JsonSerializer Default /// public virtual async Task DeserializeAsync(Stream stream, Type type, CancellationToken cancellationToken = default) => await System.Text.Json.JsonSerializer.DeserializeAsync(stream, type, this.Options, cancellationToken).ConfigureAwait(false); - /// - /// Deserializes the specified as a new - /// - /// The expected type of elements to enumerate - /// The to deserialize - /// A - /// A new + /// public virtual IAsyncEnumerable DeserializeAsyncEnumerable(Stream stream, CancellationToken cancellationToken = default) => System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable(stream, this.Options, cancellationToken); } diff --git a/src/Neuroglia.Serialization.Abstractions/Services/Interfaces/IJsonSerializer.cs b/src/Neuroglia.Serialization.Abstractions/Services/Interfaces/IJsonSerializer.cs index 6ddd6777d..ee0030042 100644 --- a/src/Neuroglia.Serialization.Abstractions/Services/Interfaces/IJsonSerializer.cs +++ b/src/Neuroglia.Serialization.Abstractions/Services/Interfaces/IJsonSerializer.cs @@ -11,6 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Text.Json.Nodes; +using System.Text.Json; + namespace Neuroglia.Serialization; /// @@ -20,6 +23,37 @@ public interface IJsonSerializer : ITextSerializer { + /// + /// Serializes the specified object into a new + /// + /// The type of object to serialize + /// The object to serialize + /// A new + JsonNode? SerializeToNode(T graph); + + /// + /// Serializes the specified object into a new + /// + /// The type of object to serialize + /// The object to serialize + /// A new + JsonElement? SerializeToElement(T graph); + + /// + /// Serializes the specified object into a new + /// + /// The type of object to serialize + /// The object to serialize + /// A new + JsonDocument? SerializeToDocument(T graph); + /// + /// Deserializes the specified as a new + /// + /// The expected type of elements to enumerate + /// The to deserialize + /// A + /// A new + IAsyncEnumerable DeserializeAsyncEnumerable(Stream stream, CancellationToken cancellationToken = default); } diff --git a/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/IServiceCollectionExtensions.cs b/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/IServiceCollectionExtensions.cs index a009a6d47..07ee4e392 100644 --- a/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/IServiceCollectionExtensions.cs +++ b/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/IServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Neuroglia.Serialization.DataContract; +using System.IO; namespace Neuroglia.Serialization; diff --git a/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/JsonSerializerExtensions.cs b/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/JsonSerializerExtensions.cs new file mode 100644 index 000000000..ba1cd17d8 --- /dev/null +++ b/src/Neuroglia.Serialization.NewtonsoftJson/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,77 @@ +// Copyright © 2021-Present Neuroglia SRL. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System.Runtime.CompilerServices; + +namespace Neuroglia.Serialization; + +/// +/// Defines extensions for s +/// +public static partial class JsonSerializerExtensions +{ + + /// + /// Wraps the JSON into an that can be used to deserialize root-level JSON arrays in a streaming manner. + /// + /// The type of items to deserialize + /// The extended + /// The to deserialize + /// A + /// A new + public static async IAsyncEnumerable DeserializeAsyncEnumerable(this JsonSerializer serializer, Stream stream, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var loadSettings = new JsonLoadSettings { LineInfoHandling = LineInfoHandling.Ignore }; + using var textReader = new StreamReader(stream, leaveOpen: true); + using var reader = new JsonTextReader(textReader) { CloseInput = false }; + await foreach (var token in LoadAsyncEnumerable(reader, loadSettings, cancellationToken).ConfigureAwait(false)) yield return token.ToObject(serializer)!; + } + + static async IAsyncEnumerable LoadAsyncEnumerable(JsonTextReader reader, JsonLoadSettings? settings = default, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + (await reader.MoveToContentAndAssertAsync().ConfigureAwait(false)).AssertTokenType(JsonToken.StartArray); + cancellationToken.ThrowIfCancellationRequested(); + while ((await reader.ReadToContentAndAssert(cancellationToken).ConfigureAwait(false)).TokenType != JsonToken.EndArray) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return await JToken.LoadAsync(reader, settings, cancellationToken).ConfigureAwait(false); + } + cancellationToken.ThrowIfCancellationRequested(); + } + + static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) => reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType)); + + static async Task ReadToContentAndAssert(this JsonReader reader, CancellationToken cancellationToken = default) => await (await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false)).MoveToContentAndAssertAsync(cancellationToken).ConfigureAwait(false); + + static async Task MoveToContentAndAssertAsync(this JsonReader reader, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(reader); + + if (reader.TokenType == JsonToken.None) await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false); + while (reader.TokenType == JsonToken.Comment) await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false); + + return reader; + } + + static async Task ReadAndAssertAsync(this JsonReader reader, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(reader); + + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) throw new JsonReaderException("Unexpected end of JSON stream."); + + return reader; + } + +} \ No newline at end of file diff --git a/src/Neuroglia.Serialization.NewtonsoftJson/Services/NewtonsoftJsonSerializer.cs b/src/Neuroglia.Serialization.NewtonsoftJson/Services/NewtonsoftJsonSerializer.cs index eceaff26c..5e325e147 100644 --- a/src/Neuroglia.Serialization.NewtonsoftJson/Services/NewtonsoftJsonSerializer.cs +++ b/src/Neuroglia.Serialization.NewtonsoftJson/Services/NewtonsoftJsonSerializer.cs @@ -11,32 +11,30 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Json.More; using Microsoft.Extensions.Options; using Newtonsoft.Json; using System.Net.Mime; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Neuroglia.Serialization.DataContract; /// /// Represents the DataContract implementation of the /// -public class NewtonsoftJsonSerializer +/// +/// Initializes a new +/// +/// The service used to monitor the current +public class NewtonsoftJsonSerializer(IOptionsMonitor settings) : IJsonSerializer { - /// - /// Initializes a new - /// - /// The service used to monitor the current - public NewtonsoftJsonSerializer(IOptionsMonitor settings) - { - this.Settings = settings; - } - /// /// Gets the service used to monitor the current /// - protected IOptionsMonitor Settings { get; } + protected IOptionsMonitor Settings { get; } = settings; /// public virtual bool Supports(string mediaTypeName) => mediaTypeName == MediaTypeNames.Application.Json || mediaTypeName.EndsWith("+json"); @@ -44,7 +42,7 @@ public NewtonsoftJsonSerializer(IOptionsMonitor settings /// public virtual void Serialize(object? value, Stream stream, Type? type = null) { - var serializer = JsonSerializer.Create(this.Settings.CurrentValue); + var serializer = Newtonsoft.Json.JsonSerializer.Create(this.Settings.CurrentValue); using var streamWriter = new StreamWriter(stream, leaveOpen: true); using var jsonTextWriter = new JsonTextWriter(streamWriter); serializer.Serialize(jsonTextWriter, value, type); @@ -59,10 +57,27 @@ public virtual void Serialize(object? value, Stream stream, Type? type = null) /// public virtual object? Deserialize(Stream stream, Type type) { - var serializer = JsonSerializer.Create(this.Settings.CurrentValue); + var serializer = Newtonsoft.Json.JsonSerializer.Create(this.Settings.CurrentValue); using var streamReader = new StreamReader(stream, leaveOpen: true); using var jsonTextReader = new JsonTextReader(streamReader); return serializer.Deserialize(jsonTextReader, type); } + /// + public virtual JsonNode? SerializeToNode(T graph) => graph == null ? null : JsonNode.Parse(this.SerializeToText(graph)); + + /// + public virtual JsonElement? SerializeToElement(T graph) + { + if (graph == null) return null; + var reader = new Utf8JsonReader(this.SerializeToByteArray(graph)); + return JsonElement.ParseValue(ref reader); + } + + /// + public virtual JsonDocument? SerializeToDocument(T graph) => graph == null ? null : JsonDocument.Parse(this.SerializeToText(graph)); + + /// + public virtual IAsyncEnumerable DeserializeAsyncEnumerable(Stream stream, CancellationToken cancellationToken = default) => Newtonsoft.Json.JsonSerializer.Create(this.Settings.CurrentValue).DeserializeAsyncEnumerable(stream, cancellationToken); + }