From e143ecd0067f0f3ce899cd64c58891e10099fe50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D1=88=D0=BC=D0=B0=D0=BD=20=D0=90=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=BD=20=D0=9B=D0=B5=D0=B9=D0=B1=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Tue, 23 Jun 2026 13:36:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D0=BA=D1=83=D1=89=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=BA=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JsonCanonicalizer.Test.csproj | 14 +++ JsonCanicalizer.Test/Program.cs | 26 ++++ JsonCanicalizer.Test/result.json | 64 ++++++++++ JsonCanicalizer.Test/test.json | 1 + JsonCanonicalizer.BL/Canonicalizer.cs | 112 ++++++++++++++++++ .../JsonCanonicalizer.BL.csproj | 9 ++ JsonCanonicalizer.sln | 37 ++++++ JsonCanonicalizer/JsonCanonicalizer.csproj | 14 +++ JsonCanonicalizer/Program.cs | 41 +++++++ nuget.config | 7 ++ 10 files changed, 325 insertions(+) create mode 100644 JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj create mode 100644 JsonCanicalizer.Test/Program.cs create mode 100644 JsonCanicalizer.Test/result.json create mode 100644 JsonCanicalizer.Test/test.json create mode 100644 JsonCanonicalizer.BL/Canonicalizer.cs create mode 100644 JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj create mode 100644 JsonCanonicalizer.sln create mode 100644 JsonCanonicalizer/JsonCanonicalizer.csproj create mode 100644 JsonCanonicalizer/Program.cs create mode 100644 nuget.config diff --git a/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj b/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj new file mode 100644 index 0000000..b0d2e8b --- /dev/null +++ b/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/JsonCanicalizer.Test/Program.cs b/JsonCanicalizer.Test/Program.cs new file mode 100644 index 0000000..c6dd3fb --- /dev/null +++ b/JsonCanicalizer.Test/Program.cs @@ -0,0 +1,26 @@ +using JsonCanonicalizer.BL; +using System.Text.Json; + +namespace JsonCanicalizer.Test; + +internal class Program +{ + static void Main(string[] args) + { + string input = File.ReadAllText(@"d:\Code\testbench\JsonCanonicalizer\JsonCanicalizer.Test\test.json"); + + using var doc = JsonDocument.Parse(input); + + var writerOptions = new JsonWriterOptions + { + Indented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + using var outputStream = new FileStream(@"d:\Code\testbench\JsonCanonicalizer\JsonCanicalizer.Test\result.json", FileMode.Create); + using var writer = new Utf8JsonWriter(outputStream, writerOptions); + + Canonicalizer.WriteSorted(writer, doc.RootElement); + writer.Flush(); + } +} diff --git a/JsonCanicalizer.Test/result.json b/JsonCanicalizer.Test/result.json new file mode 100644 index 0000000..589243f --- /dev/null +++ b/JsonCanicalizer.Test/result.json @@ -0,0 +1,64 @@ +{ + "serviceTasks": [ + { + "guid": "07e607a8-7673-458f-a8f0-2850f7864327", + "id": "Activity_1y13pct", + "name": "Внесение изменений/ дополнений/ исключений сведений в реестр уведомлений ТЭД (envelopeData)" + }, + { + "guid": "18b61142-b1e7-483a-bb17-3d42db1b8db3", + "id": "Activity_0lrt9vk", + "name": "Присвоение номера по шаблону" + }, + { + "guid": "29203f14-7505-4994-bd84-574524b35c65", + "id": "Activity_086flld", + "name": "Получение выписки из реестра ТЭД" + }, + { + "guid": "2a804098-fba1-4828-a139-c1449c3b2acf", + "id": "Activity_1etjhti", + "name": "Перевод дела в статус \"Положительное решение\"" + }, + { + "guid": "406c7066-3eb7-4abd-a7d9-b9f96cc9e236", + "id": "Activity_0ptjle4", + "name": "Включение в реестр уведомлений ТЭД (envelopeData)" + }, + { + "guid": "59b09840-2b0f-47c3-9d07-941e00dc9794", + "id": "Activity_1wcy9l4", + "name": "Открытые сведения из ЕГРЮЛ (включая сведения, доступ к которым ограничен абзацами 3 и 5 пункта 1 статьи 6 Федерального закона от 08.08.2001 № 129-ФЗ) по запросам органов государственной власти (envelopeData)" + }, + { + "guid": "805fe785-5c2b-4ee2-a2e7-c4adbc40ae5d", + "id": "Activity_0bwwih4", + "name": "Перевод в статус \"Отказ в предоставлении услуги\" (4)" + }, + { + "guid": "80dbe463-1df6-4f9f-b76d-888786c1484f", + "id": "Activity_0ii8c0k", + "name": "Перевод дела в статус \"Отрицательное решение\"" + }, + { + "guid": "87c1dc1d-b8d6-464c-8eff-48163f5b86ab", + "id": "Activity_0hwnhm8", + "name": "Отправка на ЕПГУ \"Услуга оказана\" (3) с положительным решением, необходимые файлы получены" + }, + { + "guid": "bee4c89c-601f-4211-a615-7ef8a3eeade7", + "id": "Activity_14vp8wt", + "name": "Получение данных из реестра уведомлений ТЭД (envelopeData)" + }, + { + "guid": "e51e3b7c-082c-4555-bbfa-1579c81b9ffb", + "id": "Activity_0ux39aa", + "name": "Перевод статуса заявления на ЕПГУ \"Заявление принято к рассмотрению\"(6)" + }, + { + "guid": "ee174972-ab78-4fc6-979a-37c1384f3c6d", + "id": "Activity_1yii4fr", + "name": "Сведения из ЕГРИП по запросам органов государственной власти (открытые сведения и сведения о рождении и адресе ИП) (регламентный)" + } + ] +} \ No newline at end of file diff --git a/JsonCanicalizer.Test/test.json b/JsonCanicalizer.Test/test.json new file mode 100644 index 0000000..e0d0bd1 --- /dev/null +++ b/JsonCanicalizer.Test/test.json @@ -0,0 +1 @@ +{"serviceTasks":[{"guid":"18b61142-b1e7-483a-bb17-3d42db1b8db3","id":"Activity_0lrt9vk","name":"Присвоение номера по шаблону"},{"guid":"29203f14-7505-4994-bd84-574524b35c65","id":"Activity_086flld","name":"Получение выписки из реестра ТЭД"},{"guid":"80dbe463-1df6-4f9f-b76d-888786c1484f","id":"Activity_0ii8c0k","name":"Перевод дела в статус \"Отрицательное решение\""},{"guid":"805fe785-5c2b-4ee2-a2e7-c4adbc40ae5d","id":"Activity_0bwwih4","name":"Перевод в статус \"Отказ в предоставлении услуги\" (4)"},{"guid":"87c1dc1d-b8d6-464c-8eff-48163f5b86ab","id":"Activity_0hwnhm8","name":"Отправка на ЕПГУ \"Услуга оказана\" (3) с положительным решением, необходимые файлы получены"},{"guid":"2a804098-fba1-4828-a139-c1449c3b2acf","id":"Activity_1etjhti","name":"Перевод дела в статус \"Положительное решение\""},{"guid":"e51e3b7c-082c-4555-bbfa-1579c81b9ffb","id":"Activity_0ux39aa","name":"Перевод статуса заявления на ЕПГУ \"Заявление принято к рассмотрению\"(6)"},{"guid":"ee174972-ab78-4fc6-979a-37c1384f3c6d","id":"Activity_1yii4fr","name":"Сведения из ЕГРИП по запросам органов государственной власти (открытые сведения и сведения о рождении и адресе ИП) (регламентный)"},{"guid":"59b09840-2b0f-47c3-9d07-941e00dc9794","id":"Activity_1wcy9l4","name":"Открытые сведения из ЕГРЮЛ (включая сведения, доступ к которым ограничен абзацами 3 и 5 пункта 1 статьи 6 Федерального закона от 08.08.2001 № 129-ФЗ) по запросам органов государственной власти (envelopeData)"},{"guid":"406c7066-3eb7-4abd-a7d9-b9f96cc9e236","id":"Activity_0ptjle4","name":"Включение в реестр уведомлений ТЭД (envelopeData)"},{"guid":"bee4c89c-601f-4211-a615-7ef8a3eeade7","id":"Activity_14vp8wt","name":"Получение данных из реестра уведомлений ТЭД (envelopeData)"},{"guid":"07e607a8-7673-458f-a8f0-2850f7864327","id":"Activity_1y13pct","name":"Внесение изменений/ дополнений/ исключений сведений в реестр уведомлений ТЭД (envelopeData)"}]} \ No newline at end of file diff --git a/JsonCanonicalizer.BL/Canonicalizer.cs b/JsonCanonicalizer.BL/Canonicalizer.cs new file mode 100644 index 0000000..14f6a69 --- /dev/null +++ b/JsonCanonicalizer.BL/Canonicalizer.cs @@ -0,0 +1,112 @@ +using System.Runtime.Serialization; +using System.Text.Json; + +namespace JsonCanonicalizer.BL; + +public class Canonicalizer +{ + public static void WriteSorted(Utf8JsonWriter writer, JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + writer.WriteStartObject(); + // Сортируем ключи объекта по имени + foreach (var property in element.EnumerateObject() + .OrderBy(p => p.Name, StringComparer.Ordinal)) + { + writer.WritePropertyName(property.Name); + WriteSorted(writer, property.Value); + } + writer.WriteEndObject(); + break; + + case JsonValueKind.Array: + writer.WriteStartArray(); + var items = element.EnumerateArray().ToList(); + + // Проверяем, нужно ли сортировать массив по guid: + // 1. Все элементы — объекты + // 2. У каждого объекта есть свойство "guid" + bool allAreObjects = items.All(i => i.ValueKind == JsonValueKind.Object); + bool allHaveGuid = allAreObjects && + items.All(i => IsOrderable(i)); + + IEnumerable orderedItems = items; + if (allHaveGuid) + { + orderedItems = items.OrderBy(i => GetOrder(i), + StringComparer.OrdinalIgnoreCase); + } + + // Рекурсивно обрабатываем каждый элемент (на случай вложенных массивов) + foreach (var item in orderedItems) + { + WriteSorted(writer, item); + } + writer.WriteEndArray(); + break; + + case JsonValueKind.String: + writer.WriteStringValue(element.GetString()); + break; + + case JsonValueKind.Number: + if (element.TryGetDecimal(out decimal dec)) + writer.WriteNumberValue(dec); + else + writer.WriteNumberValue(element.GetDouble()); + break; + + case JsonValueKind.True: + writer.WriteBooleanValue(true); + break; + + case JsonValueKind.False: + writer.WriteBooleanValue(false); + break; + + case JsonValueKind.Null: + writer.WriteNullValue(); + break; + + default: + writer.WriteCommentValue(element.GetRawText()); + break; + } + } + + static bool IsOrderable(JsonElement e) + { + return e.TryGetProperty("id", out _) || e.TryGetProperty("guid", out _); + } + + static string GetOrder(JsonElement e) + { + if (e.TryGetProperty("id", out JsonElement id)) + { + return GetGuidValue(id); + } + else + { + return GetGuidValue(e.GetProperty("guid")); + } + } + + /// + /// Универсально извлекает значение guid как строку для сравнения. + /// Работает даже если guid почему-то оказался числом или булевым. + /// + static string GetGuidValue(JsonElement guidElement) + { + return guidElement.ValueKind switch + { + JsonValueKind.String => guidElement.GetString() ?? "", + JsonValueKind.Number => guidElement.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + JsonValueKind.Null => "", + _ => guidElement.GetRawText() + }; + } +} diff --git a/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj b/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/JsonCanonicalizer.sln b/JsonCanonicalizer.sln new file mode 100644 index 0000000..0f9e7b4 --- /dev/null +++ b/JsonCanonicalizer.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer", "JsonCanonicalizer\JsonCanonicalizer.csproj", "{35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer.BL", "JsonCanonicalizer.BL\JsonCanonicalizer.BL.csproj", "{C4C72D1B-E361-49FB-A76D-6691D27CF2AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer.Test", "JsonCanicalizer.Test\JsonCanonicalizer.Test.csproj", "{E43C66DF-FE35-4752-8201-A08E807122DB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Release|Any CPU.Build.0 = Release|Any CPU + {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Release|Any CPU.Build.0 = Release|Any CPU + {E43C66DF-FE35-4752-8201-A08E807122DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E43C66DF-FE35-4752-8201-A08E807122DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E43C66DF-FE35-4752-8201-A08E807122DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E43C66DF-FE35-4752-8201-A08E807122DB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ADE14663-0DC6-43E9-9161-DC0A59CE0881} + EndGlobalSection +EndGlobal diff --git a/JsonCanonicalizer/JsonCanonicalizer.csproj b/JsonCanonicalizer/JsonCanonicalizer.csproj new file mode 100644 index 0000000..b0d2e8b --- /dev/null +++ b/JsonCanonicalizer/JsonCanonicalizer.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/JsonCanonicalizer/Program.cs b/JsonCanonicalizer/Program.cs new file mode 100644 index 0000000..3273ff0 --- /dev/null +++ b/JsonCanonicalizer/Program.cs @@ -0,0 +1,41 @@ +using System.Text.Json; + +namespace JsonCanonicalizer +{ + class Program + { + static int Main(string[] args) + { + try + { + using var inputStream = Console.OpenStandardInput(); + using var doc = JsonDocument.Parse(inputStream); + + var writerOptions = new JsonWriterOptions + { + Indented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + using var outputStream = Console.OpenStandardOutput(); + using var writer = new Utf8JsonWriter(outputStream, writerOptions); + + BL.Canonicalizer.WriteSorted(writer, doc.RootElement); + writer.Flush(); + + Console.Out.WriteLine(); + return 0; + } + catch (JsonException ex) + { + Console.Error.WriteLine($"Invalid JSON: {ex.Message}"); + return 1; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + } +} \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..6582799 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file