diff --git a/src/MDAT/MDAT.csproj b/src/MDAT/MDAT.csproj index 500dcca..b7d8043 100644 --- a/src/MDAT/MDAT.csproj +++ b/src/MDAT/MDAT.csproj @@ -10,7 +10,7 @@ MESS https://github.com/MTESSDev/mdAT https://github.com/MTESSDev/mdAT - 1.5.0 + 1.5.1 Markdown Auto-Tests for .net 6+, based on MSTestsV2 DataSource. diff --git a/src/MDAT/MarkdownTest.cs b/src/MDAT/MarkdownTest.cs index 3454321..04581ad 100644 --- a/src/MDAT/MarkdownTest.cs +++ b/src/MDAT/MarkdownTest.cs @@ -2,13 +2,11 @@ using Markdig; using Markdig.Syntax; using MDAT.Resolver; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.ComponentModel; using System.Reflection; using System.Text; using YamlDotNet.Core; using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; namespace MDAT { @@ -221,8 +219,8 @@ private static IDeserializer NewDeserializer(INodeTypeResolver resolver, string { DeserializerBuilder deserializer = new DeserializerBuilder() .WithTypeConverter(new ByteArayConverter(), e => e.OnBottom()) - .WithTypeConverter(new SystemTextJsonYamlTypeConverter()) - .WithTypeInspector(x => new SystemTextJsonTypeInspector(x)) + .WithTypeConverter(new SystemTextJsonYamlTypeConverter(false, true)) + .WithTypeInspector(x => new YamlDotNet.System.Text.Json.SystemTextJsonTypeInspector(x)) .WithTypeConverter(new StringValuesConverter()) .WithNodeTypeResolver(resolver) .WithNodeDeserializer(new KeyValuePairNodeDeserializer()) @@ -230,7 +228,7 @@ private static IDeserializer NewDeserializer(INodeTypeResolver resolver, string .WithAttemptingUnquotedStringTypeDeserialization() .WithTagMapping(MdatConstants.IncludeTag, typeof(IncludeRef)); - foreach(var typeConverter in MdatConfig.ListeTypeConverter) + foreach (var typeConverter in MdatConfig.ListeTypeConverter) { _ = typeConverter.Where is { } ? deserializer.WithTypeConverter(typeConverter.TypeConverter!, typeConverter.Where) : deserializer.WithTypeConverter(typeConverter.TypeConverter!); } diff --git a/src/MDAT/Resolver/JsonDocumentConverter.cs b/src/MDAT/Resolver/JsonDocumentConverter.cs deleted file mode 100644 index 7720822..0000000 --- a/src/MDAT/Resolver/JsonDocumentConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Newtonsoft.Json; -using System.Text.Json; - -namespace MDAT.Resolver -{ - public class JsonDocumentConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return typeof(JsonDocument).IsAssignableFrom(objectType); - } - - public override void WriteJson(JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer) - { - if (value is JsonDocument netJson) - { - using var reader = new JsonTextReader(new StringReader(netJson.RootElement.ToString())); - writer.WriteToken(reader); // Écrit correctement le contenu JSON dans le flux - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) - { - throw new JsonSerializationException("Invalid json document."); - } - } -} \ No newline at end of file diff --git a/src/MDAT/Resolver/SystemTextJsonFullConverter.cs b/src/MDAT/Resolver/SystemTextJsonFullConverter.cs new file mode 100644 index 0000000..ba829ec --- /dev/null +++ b/src/MDAT/Resolver/SystemTextJsonFullConverter.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MDAT.Resolver +{ + public class SystemTextJsonFullConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(JsonNode).IsAssignableFrom(objectType) + || typeof(JsonDocument).IsAssignableFrom(objectType) + || typeof(JsonElement).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + string json = value switch + { + JsonObject obj => obj.ToJsonString(), + JsonArray arr => arr.ToJsonString(), + JsonValue val => val.ToJsonString(), + JsonNode node => node.ToJsonString(), + JsonDocument doc => doc.RootElement.GetRawText(), + JsonElement elem => elem.ToString(), + _ => throw new JsonSerializationException($"Type System.Text.Json non supporté : {value.GetType()}") + }; + + writer.WriteRawValue(json); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + throw new JsonSerializationException("Invalid json document."); + } + } +} \ No newline at end of file diff --git a/src/MDAT/Resolver/SystemTextJsonYamlTypeConverter.cs b/src/MDAT/Resolver/SystemTextJsonYamlTypeConverter.cs new file mode 100644 index 0000000..6386693 --- /dev/null +++ b/src/MDAT/Resolver/SystemTextJsonYamlTypeConverter.cs @@ -0,0 +1,418 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using Scalar = YamlDotNet.Core.Events.Scalar; + +namespace MDAT.Resolver; + +/// +/// Allows YamlDotNet to de/serialize System.Text.Json objects +/// +[ExcludeFromCodeCoverage] +public sealed class SystemTextJsonYamlTypeConverter : IYamlTypeConverter +{ + private bool SortKeysAlphabetically { get; } + private bool UseInvariantCulture { get; } + + /// + /// Allows YamlDotNet to de/serialize System.Text.Json objects + /// + /// sorts keys alphabetically when Serializing + /// Force using invariant culture to respect Json values + public SystemTextJsonYamlTypeConverter(bool sortKeysAlphabetically = false, bool useInvariantCulture = false) + { + SortKeysAlphabetically = sortKeysAlphabetically; + UseInvariantCulture = useInvariantCulture; + } + + public bool Accepts(Type type) + { + return typeof(JsonNode).IsAssignableFrom(type) + || typeof(JsonArray).IsAssignableFrom(type) + || typeof(JsonObject).IsAssignableFrom(type) + || typeof(JsonValue).IsAssignableFrom(type) + || typeof(JsonElement).IsAssignableFrom(type) + || typeof(JsonDocument).IsAssignableFrom(type); + } + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (typeof(JsonValue).IsAssignableFrom(type)) + { + return ReadJsonValue(parser); + } + else if (typeof(JsonArray).IsAssignableFrom(type)) + { + return ReadJsonArray(parser, rootDeserializer); + } + else if (typeof(JsonObject).IsAssignableFrom(type) || typeof(JsonNode).IsAssignableFrom(type)) + { + return ReadJsonObject(parser, rootDeserializer); + } + else if (typeof(JsonElement).IsAssignableFrom(type)) + { + return ReadJsonDocument(parser, rootDeserializer).RootElement; + } + else if (typeof(JsonDocument).IsAssignableFrom(type)) + { + return ReadJsonDocument(parser, rootDeserializer); + } + + return null; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + if (typeof(JsonValue).IsAssignableFrom(type)) + { + WriteJsonElement(emitter, value); + } + else if (typeof(JsonObject).IsAssignableFrom(type)) + { + WriteJsonObject(emitter, value, serializer); + } + else if (typeof(JsonArray).IsAssignableFrom(type)) + { + WriteJsonArray(emitter, value, serializer); + } + else if (typeof(JsonElement).IsAssignableFrom(type)) + { + WriteJsonElement(emitter, value); + } + else if (typeof(JsonDocument).IsAssignableFrom(type)) + { + WriteJsonDocument(emitter, value); + } + } + + // Read Functions + + private object ReadJsonValue(IParser parser) + { + if (parser.TryConsume(out var scalar)) + { + if (scalar.Style == ScalarStyle.Plain) + { + if (long.TryParse(scalar.Value, out var i)) + { + return JsonValue.Create(i); + } + else if (float.TryParse(scalar.Value, NumberStyles.Float, UseInvariantCulture ? CultureInfo.InvariantCulture : null, out var f)) + { + return JsonValue.Create(f); + } + else if (bool.TryParse(scalar.Value, out var b)) + { + return JsonValue.Create(b); + } + else if (scalar.Value == "null") + { + return JsonValue.Create((object)null); + } + else if (scalar.Value.GetType() == typeof(string)) + { + return JsonValue.Create(scalar.Value); + } + } + else + { + return JsonValue.Create(scalar.Value); + } + } + + return null; + } + + private object ReadJsonObject(IParser parser, ObjectDeserializer rootDeserializer) + { + var value = ReadJsonValue(parser); + + if (value != null) + { + return value; + } + + var node = new JsonObject(); + + if (parser.TryConsume(out var start)) + { + while (!parser.Accept(out var end)) + { + var name = parser.Consume(); + + if (parser.Accept(out var scalar)) + { + node[name.Value] = (JsonValue)ReadYaml(parser, typeof(JsonValue), rootDeserializer); + } + else if (parser.Accept(out var mapStart)) + { + node[name.Value] = (JsonObject)ReadYaml(parser, typeof(JsonObject), rootDeserializer); + } + else if (parser.Accept(out var seqStart)) + { + node[name.Value] = (JsonArray)ReadYaml(parser, typeof(JsonArray), rootDeserializer); + } + } + + parser.Consume(); + } + + return node; + } + + private object ReadJsonArray(IParser parser, ObjectDeserializer rootDeserializer) + { + var array = new JsonArray(); + + if (parser.TryConsume(out var start)) + { + while (!parser.Accept(out var end)) + { + if (parser.Accept(out var scalar)) + { + array.Add((JsonValue)ReadYaml(parser, typeof(JsonValue), rootDeserializer)); + } + else if (parser.Accept(out var mapStart)) + { + array.Add((JsonObject)ReadYaml(parser, typeof(JsonObject), rootDeserializer)); + } + else if (parser.Accept(out var seqStart)) + { + array.Add((JsonArray)ReadYaml(parser, typeof(JsonArray), rootDeserializer)); + } + } + + parser.Consume(); + } + + return array; + } + + private JsonDocument ReadJsonDocument(IParser parser, ObjectDeserializer rootDeserializer) + { + if (parser.TryConsume(out var start)) + { + var node = new JsonObject(); + + while (!parser.Accept(out var end)) + { + var name = parser.Consume(); + + if (parser.Accept(out var sc)) + { + node[name.Value] = (JsonValue)ReadYaml(parser, typeof(JsonValue), rootDeserializer); + } + else if (parser.Accept(out var mapStart)) + { + node[name.Value] = (JsonObject)ReadYaml(parser, typeof(JsonObject), rootDeserializer); + } + else if (parser.Accept(out var seqStart)) + { + node[name.Value] = (JsonArray)ReadYaml(parser, typeof(JsonArray), rootDeserializer); + } + } + + parser.Consume(); + + return JsonSerializer.Deserialize(node); + } + + if (parser.TryConsume(out var start2)) + { + var array = new JsonArray(); + + while (!parser.Accept(out var end)) + { + if (parser.Accept(out var scalar2)) + { + array.Add((JsonValue)ReadYaml(parser, typeof(JsonValue), rootDeserializer)); + } + else if (parser.Accept(out var mapStart)) + { + array.Add((JsonObject)ReadYaml(parser, typeof(JsonObject), rootDeserializer)); + } + else if (parser.Accept(out var seqStart)) + { + array.Add((JsonArray)ReadYaml(parser, typeof(JsonArray), rootDeserializer)); + } + } + + parser.Consume(); + + return JsonSerializer.SerializeToDocument(array); + } + + if (parser.TryConsume(out var scalar)) + { + if (scalar.Style == ScalarStyle.Plain) + { + if (long.TryParse(scalar.Value, out var i)) + { + return JsonSerializer.SerializeToDocument(i); + } + else if (float.TryParse(scalar.Value, out var f)) + { + return JsonSerializer.SerializeToDocument(f); + } + else if (bool.TryParse(scalar.Value, out var b)) + { + return JsonSerializer.SerializeToDocument(b); + } + else if (scalar.Value == "null") + { + return JsonSerializer.SerializeToDocument((object)null); + } + else if (scalar.Value.GetType() == typeof(string)) + { + return JsonSerializer.SerializeToDocument(scalar.Value); + } + } + else + { + return JsonSerializer.SerializeToDocument(scalar.Value); + } + } + + return null; + } + + // Write Functions + + private void WriteJsonObject(IEmitter emitter, object value, ObjectSerializer serializer) + { + emitter.Emit(new MappingStart(null, null, false, MappingStyle.Any)); + + foreach (var property in SortKeysAlphabetically ? ((JsonObject)value).OrderBy(x => x.Key).ToArray() : ((JsonObject)value).ToArray()) + { + JsonNode propVal = property.Value; + + emitter.Emit(new Scalar(null, property.Key)); + + if (property.Value == null) + { + WriteJsonElement(emitter, null); + } + else + { + WriteYaml(emitter, propVal, propVal.GetType(), serializer); + } + } + + emitter.Emit(new MappingEnd()); + } + + private void WriteJsonDocument(IEmitter emitter, object value) + { + var doc = (JsonDocument)value; + + var root = doc.RootElement; + + WriteJsonElement(emitter, root); + } + + private void WriteJsonElement(IEmitter emitter, object value) + { + if (value == null) + { + emitter.Emit(new Scalar(null, "null")); + return; + } + + JsonElement obj; + + if (typeof(JsonElement).IsAssignableFrom(value.GetType())) + { + obj = (JsonElement)value; + } + else + { + obj = ((JsonValue)value).Deserialize(); + } + + switch (obj.ValueKind) + { + case JsonValueKind.Object: + emitter.Emit(new MappingStart(null, null, false, MappingStyle.Any)); + + foreach (var item in SortKeysAlphabetically ? obj.EnumerateObject().OrderBy(x => x.Name).ToArray() : obj.EnumerateObject().ToArray()) + { + emitter.Emit(new Scalar(null, item.Name)); + + WriteJsonElement(emitter, item.Value); + } + emitter.Emit(new MappingEnd()); + + break; + case JsonValueKind.Array: + emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Any)); + + foreach (var item in obj.EnumerateArray()) + { + WriteJsonElement(emitter, item); + } + + emitter.Emit(new SequenceEnd()); + + break; + case JsonValueKind.String: + var val = obj.ToString(); + + if (val.IndexOf("\n") > 0) + { + // force it to be multi-line literal (aka |) + emitter.Emit(new Scalar(null, null, val, ScalarStyle.Literal, true, true)); + } + else + { + // if string could be interpreted as a non-string value type, put quotes around it. + if (val == "null" || + long.TryParse(val, out var _) || + float.TryParse(val, out var _) || + decimal.TryParse(val, out var _) || + bool.TryParse(val, out var _)) + { + emitter.Emit(new Scalar(null, null, val, ScalarStyle.SingleQuoted, true, true)); + } + else + { + emitter.Emit(new Scalar(val)); + } + } + break; + case JsonValueKind.Number: + emitter.Emit(new Scalar(obj.ToString())); + break; + case JsonValueKind.True: + case JsonValueKind.False: + emitter.Emit(new Scalar(obj.ToString().ToLower())); + break; + case JsonValueKind.Null: + emitter.Emit(new Scalar(null, "null")); + break; + } + } + + private void WriteJsonArray(IEmitter emitter, object value, ObjectSerializer serializer) + { + var style = SequenceStyle.Any; + + emitter.Emit(new SequenceStart(null, null, false, style)); + + foreach (var item in ((JsonArray)value)) + { + if (item == null) + { + emitter.Emit(new Scalar(null, "null")); + continue; + } + + WriteYaml(emitter, item, item.GetType(), serializer); + } + + emitter.Emit(new SequenceEnd()); + } +} \ No newline at end of file diff --git a/src/MDAT/Utils.cs b/src/MDAT/Utils.cs index 832d4e7..5d48d97 100644 --- a/src/MDAT/Utils.cs +++ b/src/MDAT/Utils.cs @@ -116,7 +116,7 @@ public static JsonSerializerSettings JsonSerializeSettings() return new JsonSerializerSettings { ContractResolver = new NoResolver(), - Converters = new List { new StreamToBase64Converter(), new JsonDocumentConverter() }, + Converters = new List { new StreamToBase64Converter(), new SystemTextJsonFullConverter() }, Formatting = Formatting.Indented }; } diff --git a/src/MDATTests/MarkdownTestAttributeTests.cs b/src/MDATTests/MarkdownTestAttributeTests.cs index 5d6c01e..9de0eff 100644 --- a/src/MDATTests/MarkdownTestAttributeTests.cs +++ b/src/MDATTests/MarkdownTestAttributeTests.cs @@ -1,11 +1,7 @@ using MDATTests; using MDATTests.Models; using Microsoft.Extensions.Primitives; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using System; -using System.Text.Json; -using static MDAT.Tests.MarkdownTestAttributeTests; //[assembly: TestDataSourceDiscovery(TestDataSourceDiscoveryOption.DuringExecution)] @@ -239,27 +235,6 @@ public async Task Md_Extraction_test(Expected expected) Task.FromResult(File.ReadAllText(test.ParsedPath)), expected); } - /// - /// Test JsonDocument Load - /// This test whole document integrity - /// - [TestMethod] - [MarkdownTest("~\\Tests\\md-json-document-test.md")] - public async Task Md_JsonDocument_test(JsonDocument jsonDocument, Test1 test1, Expected expected, Expected expected2) - { - object value = await Verify.Assert(() => - Task.FromResult(jsonDocument), expected); - - object value2 = await Verify.Assert(() => - Task.FromResult(test1), expected2); - - } - - public class Test1 - { - public JsonDocument JsonDocument { get; set; } - } - /// /// Test tests naming values /// diff --git a/src/MDATTests/NewtonsoftConvertersTests.cs b/src/MDATTests/NewtonsoftConvertersTests.cs new file mode 100644 index 0000000..3f5e50f --- /dev/null +++ b/src/MDATTests/NewtonsoftConvertersTests.cs @@ -0,0 +1,80 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MDAT.Tests; + +[TestClass] +public class NewtonsoftConvertersTests +{ + /// + /// Test JsonDocument Load + /// This test whole document integrity + /// + [TestMethod] + [MarkdownTest("~\\Tests\\md-json-document-test.md")] + public async Task Md_JsonDocument_test(JsonDocument jsonDocument, Test1 test1, Expected expected, Expected expected2) + { + object value = await Verify.Assert(() => + Task.FromResult(jsonDocument), expected); + + object value2 = await Verify.Assert(() => + Task.FromResult(test1), expected2); + + } + + /// + /// Test SystemTextJsonFullConverter - JsonNode + /// + [TestMethod] + [DoNotParallelize] + [MarkdownTest("~\\Tests\\{method}.md")] + public async Task Test_JsonNode(List nodes, Expected expected) + { + _ = await Verify.Assert(() => + Task.FromResult(nodes), expected); + } + + /// + /// Test SystemTextJsonFullConverter - JsonValue + /// + [TestMethod] + [DoNotParallelize] + [MarkdownTest("~\\Tests\\{method}.md")] + public async Task Test_JsonValue(List values, Expected expected) + { + _ = await Verify.Assert(() => + Task.FromResult(values), expected); + } + + /// + /// Test SystemTextJsonFullConverter - JsonArray + /// + [TestMethod] + [DoNotParallelize] + [MarkdownTest("~\\Tests\\{method}.md")] + public async Task Test_JsonArray(JsonArray array, Expected expected) + { + _ = await Verify.Assert(() => + Task.FromResult(array), expected); + } + + /// + /// Test SystemTextJsonFullConverter - JsonElement + /// + [TestMethod] + [DoNotParallelize] + [MarkdownTest("~\\Tests\\{method}.md")] + public async Task Test_JsonElement(JsonDocument doc, Expected expected) + { + JsonElement elem = doc.RootElement; // un objet complet + + _ = await Verify.Assert(() => + Task.FromResult(elem), expected); + } + + + public class Test1 + { + public JsonDocument JsonDocument { get; set; } + } +} diff --git a/src/MDATTests/Tests/test-json-array.md b/src/MDATTests/Tests/test-json-array.md new file mode 100644 index 0000000..e1c11d0 --- /dev/null +++ b/src/MDATTests/Tests/test-json-array.md @@ -0,0 +1,25 @@ +# Test_JsonArray + +> Test SystemTextJsonFullConverter - JsonArray + +## Case 1 + +Description + +``````yaml +array: + - t1 + - t2 + - t3 +expected: + name: null + generateExpectedData: null + verify: + - type: match + jsonPath: $ + allowAdditionalProperties: false + data: + - t1 + - t2 + - t3 +`````` \ No newline at end of file diff --git a/src/MDATTests/Tests/test-json-element.md b/src/MDATTests/Tests/test-json-element.md new file mode 100644 index 0000000..dc4f4c1 --- /dev/null +++ b/src/MDATTests/Tests/test-json-element.md @@ -0,0 +1,27 @@ +# Test_JsonElement + +> Test SystemTextJsonFullConverter - JsonElement + +## Case 1 + +Description + +``````yaml +element: + ceci: + est: + - un + - objet : complet +expected: + name: null + generateExpectedData: null + verify: + - type: match + jsonPath: $ + allowAdditionalProperties: false + data: + ceci: + est: + - un + - objet : complet +`````` \ No newline at end of file diff --git a/src/MDATTests/Tests/test-json-node.md b/src/MDATTests/Tests/test-json-node.md new file mode 100644 index 0000000..529d1d3 --- /dev/null +++ b/src/MDATTests/Tests/test-json-node.md @@ -0,0 +1,23 @@ +# Test_JsonNode + +> Test SystemTextJsonFullConverter + +## Case 1 + +Description + +``````yaml +nodes: + - 1.2 + - test +expected: + name: null + generateExpectedData: null + verify: + - type: match + jsonPath: $ + allowAdditionalProperties: false + data: + - 1.2 + - test +`````` \ No newline at end of file diff --git a/src/MDATTests/Tests/test-json-value.md b/src/MDATTests/Tests/test-json-value.md new file mode 100644 index 0000000..696b0cc --- /dev/null +++ b/src/MDATTests/Tests/test-json-value.md @@ -0,0 +1,23 @@ +# Test_JsonValue + +> Test SystemTextJsonFullConverter - JsonValue + +## Case 1 + +Description + +``````yaml +values: + - 2.923923 + - "test23" +expected: + name: null + generateExpectedData: null + verify: + - type: match + jsonPath: $ + allowAdditionalProperties: false + data: + - 2.923923 + - "test23" +`````` \ No newline at end of file