From 10ecae4e912813bd862963f89c8e08e94e1e0bc7 Mon Sep 17 00:00:00 2001 From: Latency McLaughlin Date: Thu, 15 Feb 2024 14:44:01 -0700 Subject: [PATCH] Update target framework version .NET 9.0 --- JsonEasyNavigation.sln | 31 ++ README.md | 278 ++++++++------ Unit Tests/ArrayTests.cs | 138 +++++++ Unit Tests/CachedPropertiesTests.cs | 74 ++++ Unit Tests/ExtensionsTests.cs | 64 ++++ Unit Tests/JsonEasyNavigation.Tests.csproj | 17 + Unit Tests/MappingTests.cs | 112 ++++++ Unit Tests/NumberTests.cs | 164 +++++++++ Unit Tests/PropertyTests.cs | 281 ++++++++++++++ ...lePropertyOrderAndCachedPropertiesTests.cs | 95 +++++ Unit Tests/StablePropertyOrderTests.cs | 104 ++++++ Unit Tests/ValueKindsTests.cs | 213 +++++++++++ src/ArrayEnumeratorWrapper.cs | 25 ++ src/BoxingSafeConverter.cs | 20 + src/JsonEasyNavigation.Tests/ArrayTests.cs | 144 -------- .../CachedPropertiesTests.cs | 80 ---- .../ExtensionsTests.cs | 67 ---- .../JsonEasyNavigation.Tests.csproj | 27 -- src/JsonEasyNavigation.Tests/MappingTests.cs | 109 ------ src/JsonEasyNavigation.Tests/NumberTests.cs | 167 --------- src/JsonEasyNavigation.Tests/PropertyTests.cs | 306 ---------------- ...lePropertyOrderAndCachedPropertiesTests.cs | 103 ------ .../StablePropertyOrderTests.cs | 115 ------ .../ValueKindsTests.cs | 223 ------------ src/JsonEasyNavigation.csproj | 19 + src/JsonEasyNavigation.sln | 22 -- .../ArrayEnumeratorWrapper.cs | 38 -- src/JsonEasyNavigation/BoxingSafeConverter.cs | 26 -- .../JsonEasyNavigation.csproj | 42 --- src/JsonEasyNavigation/JsonExtensions.cs | 118 ------ ...sonNavigationElement.ReadOnlyDictionary.cs | 137 ------- .../JsonNavigationElement.ReadOnlyList.cs | 113 ------ .../JsonNavigationElement.cs | 344 ------------------ .../ObjectEnumeratorWrapper.cs | 60 --- .../PrimitiveValueExtractor.cs | 112 ------ src/JsonEasyNavigation/PropertyPathVisitor.cs | 36 -- src/JsonExtensions.cs | 82 +++++ ...sonNavigationElement.ReadOnlyDictionary.cs | 96 +++++ src/JsonNavigationElement.ReadOnlyList.cs | 98 +++++ src/JsonNavigationElement.cs | 311 ++++++++++++++++ src/ObjectEnumeratorWrapper.cs | 51 +++ src/PrimitiveValueExtractor.cs | 107 ++++++ src/PropertyPathVisitor.cs | 27 ++ src/README_BODY.md | 120 ++++++ 44 files changed, 2423 insertions(+), 2493 deletions(-) create mode 100644 JsonEasyNavigation.sln create mode 100644 Unit Tests/ArrayTests.cs create mode 100644 Unit Tests/CachedPropertiesTests.cs create mode 100644 Unit Tests/ExtensionsTests.cs create mode 100644 Unit Tests/JsonEasyNavigation.Tests.csproj create mode 100644 Unit Tests/MappingTests.cs create mode 100644 Unit Tests/NumberTests.cs create mode 100644 Unit Tests/PropertyTests.cs create mode 100644 Unit Tests/StablePropertyOrderAndCachedPropertiesTests.cs create mode 100644 Unit Tests/StablePropertyOrderTests.cs create mode 100644 Unit Tests/ValueKindsTests.cs create mode 100644 src/ArrayEnumeratorWrapper.cs create mode 100644 src/BoxingSafeConverter.cs delete mode 100644 src/JsonEasyNavigation.Tests/ArrayTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/CachedPropertiesTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/ExtensionsTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/JsonEasyNavigation.Tests.csproj delete mode 100644 src/JsonEasyNavigation.Tests/MappingTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/NumberTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/PropertyTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/StablePropertyOrderAndCachedPropertiesTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/StablePropertyOrderTests.cs delete mode 100644 src/JsonEasyNavigation.Tests/ValueKindsTests.cs create mode 100644 src/JsonEasyNavigation.csproj delete mode 100644 src/JsonEasyNavigation.sln delete mode 100644 src/JsonEasyNavigation/ArrayEnumeratorWrapper.cs delete mode 100644 src/JsonEasyNavigation/BoxingSafeConverter.cs delete mode 100644 src/JsonEasyNavigation/JsonEasyNavigation.csproj delete mode 100644 src/JsonEasyNavigation/JsonExtensions.cs delete mode 100644 src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyDictionary.cs delete mode 100644 src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyList.cs delete mode 100644 src/JsonEasyNavigation/JsonNavigationElement.cs delete mode 100644 src/JsonEasyNavigation/ObjectEnumeratorWrapper.cs delete mode 100644 src/JsonEasyNavigation/PrimitiveValueExtractor.cs delete mode 100644 src/JsonEasyNavigation/PropertyPathVisitor.cs create mode 100644 src/JsonExtensions.cs create mode 100644 src/JsonNavigationElement.ReadOnlyDictionary.cs create mode 100644 src/JsonNavigationElement.ReadOnlyList.cs create mode 100644 src/JsonNavigationElement.cs create mode 100644 src/ObjectEnumeratorWrapper.cs create mode 100644 src/PrimitiveValueExtractor.cs create mode 100644 src/PropertyPathVisitor.cs create mode 100644 src/README_BODY.md diff --git a/JsonEasyNavigation.sln b/JsonEasyNavigation.sln new file mode 100644 index 0000000..39cdff0 --- /dev/null +++ b/JsonEasyNavigation.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# 17 +VisualStudioVersion = 17.11.35303.130 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonEasyNavigation.Tests", "Unit Tests\JsonEasyNavigation.Tests.csproj", "{5F8113CF-1160-4D4B-8E9A-20D36CD79F04}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonEasyNavigation", "src\JsonEasyNavigation.csproj", "{69BBD80C-AAEE-4E98-91E2-867F709A00E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Release|Any CPU.Build.0 = Release|Any CPU + {69BBD80C-AAEE-4E98-91E2-867F709A00E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69BBD80C-AAEE-4E98-91E2-867F709A00E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69BBD80C-AAEE-4E98-91E2-867F709A00E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69BBD80C-AAEE-4E98-91E2-867F709A00E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D5779526-0E6F-4772-82F5-0D41E7B0C8A1} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index b116e0a..1699a38 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,174 @@ -![JsonEasyNavigation](./media/logo.png "JsonEasyNavigation") - -![.NET](https://github.com/sharkadi-a/JsonEasyNavigation/actions/workflows/dotnet.yml/badge.svg) -[![nuget](https://img.shields.io/nuget/v/JsonEasyNavigation)](https://www.nuget.org/packages/JsonEasyNavigation) - -# JsonEasyNavigation - -This library provides a wrapper class around Microsoft's .NET JsonElement JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. -Target frameworks are .NET 5 and NET Standard 2.0. - -Here is an example: - -```JSON -{ - "Persons": [ - { - "Id": 0, - "Name": "John", - "SecondName": "Wick", - "NickName": "Baba Yaga" - }, - { - "Id": 1, - "Name": "Wade", - "SecondName": "Winston", - "NickName": "Deadpool" - } - ] -} -``` - -Assume that we are using `System.Text.Json` so we can create JsonDocument: - -```C# -var jsonDocument = JsonDocument.Parse(json); -``` - -Then we convert this JSON document to the `JsonNavigationElement` provided by JsonEasyNavigation library: - -```C# -var nav = jsonDocument.ToNavigation(); -``` - -`JsonNavigationElement` is a struct, a wrapper around JsonElement. This struct provides many useful methods to operate arrays, objects and getting values from the JsonElement inside. - -Now we can easley navigate Domain Object Model using indexers in a sequential style: - -```C# -var arrayItem = nav[0]; // first item in the array -var id = arrayItem["Id"].GetInt32OrDefault(); // 0 -var nickName = arrayItem["NickName"].GetStringOrDefault(); // "Baba Yaga" -``` - -Notice the usage of `GetXxxOrDefault` methods, which provides a convenient way to get values from the JsonElement without throwing exceptions. There are a lot of other similar useful methods. - -We also can check if the property exist: - -```C# -if (nav[0]["Age"].Exist) -{ - // Do something if the Age property of the first object in array exist. -} -``` - -`JsonNavigationElement` does **not** throw exception if a property or array item does not exist. You can always check `Exist` property of an `JsonNavigationElement` to be sure that corresponding `JsonElement` was found. - -It is also possible to map `JsonNavigationElement` into the object of the specific type (using JsonSerializer internally): - -```C# -public class Person -{ - public int Id { get; set; } - public string Name { get; set; } - public string SecondName { get; set; } - public string NickName { get; set; } -} - -// ... - -var person = nav[0].Map(); -``` - -## Installation - -You can search nuget.org for `JsonEasyNavigation` package or download it directly from . See this link for more information. - -## Features - -Overall, the library provides following features: - -* A wrapper around JsonElement encapsulating all behaviour regarding to the DOM traversal and navigation (`JsonNavigationElement`); -* The API is implemented in a no-throw manner - you can "get" properties that don't exist in the DOM and check their existence; -* Implementation of a `IReadOnlyDictionary` and `IReadOnlyCollection`; -* Methods for converting values to the specified types in a type-safe way (and also generic methods like `TryGetValue`); -* Extensions for caching properties and persisting their order for faster and easier JSON navigation. - -## Contributing - -If you would like to fix some bugs and add new features (in a non-breaking manner) you are free to send pull-requests. - -## License - -This software is distributed under the Apache License 2.0. See LICENSE.txt. - +# JsonEasyNavigation +This library provides a wrapper class around JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. + + +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description
UPDATED:9/17/2024
FRAMEWORK:netstandard2.0, netstandard2.1, net9.0
LANGUAGE:[C#] preview
OUTPUT TYPE:Library [API]
SUPPORTS:[Visual Studio]
GFX SUBSYS:[None]
TAGS:[text.json json navigation indexer dictionary list collection DOM]
STATUS:
LICENSE:
VERSION:
+ + +![JsonEasyNavigation](media/logo.png "JsonEasyNavigation") + +
+ +## Navigation +* Introduction +* Usage +* Features +* Installation +* License + +
+ +

Introduction

+ +This library provides a wrapper class around Microsoft's .NET JsonElement JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. +Target frameworks are .NET 5 and NET Standard 2.0. + +

Usage

+ +Here is an example: + +```JSON +{ + "Persons": [ + { + "Id": 0, + "Name": "John", + "SecondName": "Wick", + "NickName": "Baba Yaga" + }, + { + "Id": 1, + "Name": "Wade", + "SecondName": "Winston", + "NickName": "Deadpool" + } + ] +} +``` + +Assume that we are using `System.Text.Json` so we can create JsonDocument: + +```C# +var jsonDocument = JsonDocument.Parse(json); +``` + +Then we convert this JSON document to the `JsonNavigationElement` provided by JsonEasyNavigation library: + +```C# +var nav = jsonDocument.ToNavigation(); +``` + +`JsonNavigationElement` is a struct, a wrapper around JsonElement. This struct provides many useful methods to operate arrays, objects and getting values from the JsonElement inside. + +Now we can easley navigate Domain Object Model using indexers in a sequential style: + +```C# +var arrayItem = nav[0]; // first item in the array +var id = arrayItem["Id"].GetInt32OrDefault(); // 0 +var nickName = arrayItem["NickName"].GetStringOrDefault(); // "Baba Yaga" +``` + +Notice the usage of `GetXxxOrDefault` methods, which provides a convenient way to get values from the JsonElement without throwing exceptions. There are a lot of other similar useful methods. + +We also can check if the property exist: + +```C# +if (nav[0]["Age"].Exist) +{ + // Do something if the Age property of the first object in array exist. +} +``` + +`JsonNavigationElement` does **not** throw exception if a property or array item does not exist. You can always check `Exist` property of an `JsonNavigationElement` to be sure that corresponding `JsonElement` was found. + +It is also possible to map `JsonNavigationElement` into the object of the specific type (using JsonSerializer internally): + +```C# +public class Person +{ + public int Id { get; set; } + public string Name { get; set; } + public string SecondName { get; set; } + public string NickName { get; set; } +} + +// ... + +var person = nav[0].Map(); +``` + +

Features

+ +Overall, the library provides following features: + +* A wrapper around JsonElement encapsulating all behaviour regarding to the DOM traversal and navigation (`JsonNavigationElement`); +* The API is implemented in a no-throw manner - you can "get" properties that don't exist in the DOM and check their existence; +* Implementation of a `IReadOnlyDictionary` and `IReadOnlyCollection`; +* Methods for converting values to the specified types in a type-safe way (and also generic methods like `TryGetValue`); +* Extensions for caching properties and persisting their order for faster and easier JSON navigation. + +

Installation

+ +This library can be installed using NuGet found [here](https://www.nuget.org/packages/JsonEasyNavigation/). +This library can be installed using GitHub found [here](https://github.com/users/Latency/packages/nuget/package/JsonEasyNavigation). + +

License

+ +This software is distributed under the Apache License 2.0. See [LICENSE].txt. + +All graphical assets are licensed under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). + +[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job.) + + [Creative Commons Attribution 3.0 Unported License]: + [LICENSE]: + [MSDN article]: diff --git a/Unit Tests/ArrayTests.cs b/Unit Tests/ArrayTests.cs new file mode 100644 index 0000000..849da3b --- /dev/null +++ b/Unit Tests/ArrayTests.cs @@ -0,0 +1,138 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class ArrayTests +{ + [Fact] + public void GetArrayItem_ShouldSucceed() + { + const string json = """[ "item1", "item2" ]"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item1 = nav[0]; + var item2 = nav[1]; + + nav.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); + nav.Count.ShouldBe(2); + + item1.Exist.ShouldBeTrue(); + item1.Index.ShouldBe(0); + item1.GetStringOrEmpty().ShouldBe("item1"); + + item2.Exist.ShouldBeTrue(); + item2.Index.ShouldBe(1); + item2.GetStringOrEmpty().ShouldBe("item2"); + } + + [Fact] + public void WhenEmptyArray_ShouldSucceed() + { + const string json = "[ ]"; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + nav.Exist.ShouldBeTrue(); + nav.IsNullValue.ShouldBeFalse(); + nav.Count.ShouldBe(0); + } + + [Fact] + public void ArrayCount_ShouldSucceed() + { + const string json = """[ "item1", "item2", "item3", "item4" ]"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + nav.Count.ShouldBe(4); + } + + [Fact] + public void GetNonExistingArrayItem_ShouldSucceed() + { + const string json = """[ "item1", "item2" ]"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + nav[nav.Count].Exist.ShouldBeFalse(); + } + + [Fact] + public void WhenIndexOutOfRange_ShouldThrow() + { + const string json = """[ "item1", "item2" ]"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + Should.Throw(() => nav[-1].Exist); + } + + [Fact] + public void WhenArrayInProperty_ShouldSucceed() + { + const string json = """{ "item1": 1, "item2": 2, "item3": [ "item31", "item32", "item33" ] }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item3"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item3"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); + item.Count.ShouldBe(3); + + item[0].GetStringOrEmpty().ShouldBe("item31"); + item[1].GetStringOrEmpty().ShouldBe("item32"); + item[2].GetStringOrEmpty().ShouldBe("item33"); + } + + [Fact] + public void WhenArrayInArray_ShouldSucceed() + { + const string json = """[ "item1", "item2", [ "item31", "item32" ] ]"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav[2]; + item.Exist.ShouldBeTrue(); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); + item.Count.ShouldBe(2); + + var inner = item[1]; + inner.Exist.ShouldBeTrue(); + inner.GetStringOrEmpty().ShouldBe("item32"); + } + + private const string Json = """[ "item1", "item2", "item3" ]"""; + + [Fact] + public void WhenEnumeratingKey_ShouldBeEmpty() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav.Keys.ShouldBeEmpty(); + } + + [Fact] + public void EnumerateArray_ShouldEnumerateItems() + { + + + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + var list = new List(); + foreach (var item in nav) + list.Add(item.GetStringOrEmpty()); + list.ShouldBe(["item1", "item2", "item3"]); + } +} \ No newline at end of file diff --git a/Unit Tests/CachedPropertiesTests.cs b/Unit Tests/CachedPropertiesTests.cs new file mode 100644 index 0000000..05c5884 --- /dev/null +++ b/Unit Tests/CachedPropertiesTests.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class CachedPropertiesTests +{ + [Fact] + public void WithCachedProperties_ShouldSucceed() + { + const string json = """{ "item3": 3, "item1": 1, "item4": 4}"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item1"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item1"); + item.GetInt32OrDefault().ShouldBe(1); + } + + private const string Json = """{ "item3": 3, "item1": 1, "item4": { "item42" : 42, "item41": 41 } }"""; + + [Fact] + public void WithCachedPropertiesAndDescendants_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item4"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item4"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + var innerItem = item["item41"]; + innerItem.Exist.ShouldBeTrue(); + innerItem.Name.ShouldBe("item41"); + innerItem.GetInt32OrDefault().ShouldBe(41); + } + + [Fact] + public void WithoutCachedProperties_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithoutCachedProperties(); + + var item = nav["item4"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item4"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + var innerItem = item["item41"]; + innerItem.Exist.ShouldBeTrue(); + innerItem.Name.ShouldBe("item41"); + innerItem.GetInt32OrDefault().ShouldBe(41); + } + + [Fact] + public void WithThenWithoutCachedProperties_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithCachedProperties().WithoutCachedProperties(); + + var item = nav["item4"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item4"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + var innerItem = item["item41"]; + innerItem.Exist.ShouldBeTrue(); + innerItem.Name.ShouldBe("item41"); + innerItem.GetInt32OrDefault().ShouldBe(41); + } +} \ No newline at end of file diff --git a/Unit Tests/ExtensionsTests.cs b/Unit Tests/ExtensionsTests.cs new file mode 100644 index 0000000..5aa12bb --- /dev/null +++ b/Unit Tests/ExtensionsTests.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class ExtensionsTests +{ + [Fact] + public void UsingPath_ShouldFindProperty() + { + const string json = """{ "item1" : { "item2" : { "item3" : "test-string" } } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.RootElement.Property("item1", "item2", "item3"); + + nav.Exist.ShouldBeTrue(); + nav.GetStringOrDefault().ShouldBe("test-string"); + } + + [Fact] + public void UsingPath_ShouldNotFindProperty() + { + const string json = """{ "item1" : { "item2" : { "item3" : "test-string" } } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.RootElement.Property("item1", "item2", "item3", "item4"); + + nav.Exist.ShouldBeFalse(); + } + + [Fact] + public void UsingPathAndGetProperty_ShouldFindProperty() + { + const string json = """{ "item1" : { "item2" : { "item3" : { "item4" : 100 } } } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.RootElement.Property("item1", "item2", "item3")["item4"]; + + nav.Exist.ShouldBeTrue(); + nav.GetInt32OrDefault().ShouldBe(100); + } + + [Fact] + public void SelectFromManyJsonSources_SumShouldBeCorrect() + { + const string json1 = """{ "item3": 3, "item1": 10, "item4": 4}"""; + const string json2 = """{ "item3": 3, "item1": 15, "item4": 4}"""; + const string json3 = """{ "item3": 3, "item1": 99, "item4": 4}"""; + + var jsonDocuments = new[] + { + JsonDocument.Parse(json1), + JsonDocument.Parse(json2), + JsonDocument.Parse(json3), + }; + + var sum = jsonDocuments + .NavigateSelect(x => x["item1"]) + .Select(x => x.GetInt32OrDefault()) + .Sum(); + + sum.ShouldBe(10 + 15 + 99); + } +} \ No newline at end of file diff --git a/Unit Tests/JsonEasyNavigation.Tests.csproj b/Unit Tests/JsonEasyNavigation.Tests.csproj new file mode 100644 index 0000000..0d65d28 --- /dev/null +++ b/Unit Tests/JsonEasyNavigation.Tests.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + true + enable + + + + + + + + + + + diff --git a/Unit Tests/MappingTests.cs b/Unit Tests/MappingTests.cs new file mode 100644 index 0000000..852332a --- /dev/null +++ b/Unit Tests/MappingTests.cs @@ -0,0 +1,112 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class MappingTests +{ + [Serializable] + public class SimpleObject + { + public int Item1 { get; set; } + } + + [Serializable] + public class HierarchyObject + { + [Serializable] + public class InnerObject + { + public decimal Inner { get; set; } + } + + public int Item1 { get; set; } + public string? Item2 { get; set; } + public InnerObject? Item3 { get; set; } + } + + [Serializable] + public class HierarchyObjectWithArray + { + [Serializable] + public class InnerObject + { + public decimal Item1 { get; set; } + public string? Item2 { get; set; } + public InnerObject[]? InnerArray { get; set; } + } + + public InnerObject[]? Array { get; set; } + } + + [Fact] + public void SimpleObjectMapping_ShouldSucceed() + { + const string json = """{"item1": 2021}"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var obj = nav.Map(); + obj.ShouldNotBeNull(); + obj.Item1.ShouldBe(2021); + } + + private const string Json = """{"item1": 2021, "item2": "string", "item3": {"inner": 123.321} }"""; + + [Fact] + public void HierarchyObjectMapping_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + var obj = nav.Map(); + obj.ShouldNotBeNull(); + obj.Item1.ShouldBe(2021); + obj.Item2.ShouldBe("string"); + obj.Item3.ShouldNotBeNull(); + obj.Item3.Inner.ShouldBe(123.321M); + } + + [Fact] + public void HierarchySubObjectMapping_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + var obj = nav["item3"].Map(); + obj.ShouldNotBeNull(); + obj.Inner.ShouldBe(123.321M); + } + + [Fact] + public void HierarchyWithArrayMapping_ShouldSucceed() + { + const string json = """ + { "array": [ + { "item1": 123.321, "item2": "string", + "innerArray": [ + { "item1": 999.111, "item2": "anotherString", "innerArray" : [] } + ]} ] } + """; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var obj = nav.Map(); + obj.ShouldNotBeNull(); + obj.Array.ShouldNotBeEmpty(); + foreach (var o in obj.Array) + { + o.Item1.ShouldBe(123.321M); + o.Item2.ShouldBe("string"); + o.InnerArray.ShouldNotBeEmpty(); + foreach (var innerObject in o.InnerArray) + { + innerObject.Item1.ShouldBe(999.111M); + innerObject.Item2.ShouldBe("anotherString"); + innerObject.InnerArray.ShouldBeEmpty(); + } + } + } +} \ No newline at end of file diff --git a/Unit Tests/NumberTests.cs b/Unit Tests/NumberTests.cs new file mode 100644 index 0000000..a605d93 --- /dev/null +++ b/Unit Tests/NumberTests.cs @@ -0,0 +1,164 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class NumberTests +{ + [Fact] + public void WhenCorrectNumber_ShouldSucceed() + { + const string json = """{ "item": 42 }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetInt16OrDefault().ShouldBe((short)42); + item.GetInt32OrDefault().ShouldBe(42); + item.GetInt64OrDefault().ShouldBe(42); + item.GetUInt16OrDefault().ShouldBe((ushort)42); + item.GetUInt32OrDefault().ShouldBe((uint)42); + item.GetUInt64OrDefault().ShouldBe((ulong)42); + item.GetByteOrDefault().ShouldBe((byte)42); + item.GetDecimalOrDefault().ShouldBe(42); + item.GetDoubleOrDefault().ShouldBe(42); + item.GetSingleOrDefault().ShouldBe(42); + + item.GetValueOrDefault().ShouldBe(42); + item.GetValueOrDefault().ShouldBe((short)42); + item.GetValueOrDefault().ShouldBe(42); + item.GetValueOrDefault().ShouldBe((uint)42); + item.GetValueOrDefault().ShouldBe((ushort)42); + item.GetValueOrDefault().ShouldBe((ulong)42); + item.GetValueOrDefault().ShouldBe((byte)42); + item.GetValueOrDefault().ShouldBe(42); + item.GetValueOrDefault().ShouldBe(42); + item.GetValueOrDefault().ShouldBe(42); + } + + [Fact] + public void WhenSignedNumberUsed_ShouldSucceed() + { + const string json = """{ "item": -15 }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetInt16OrDefault().ShouldBe((short)-15); + item.GetInt32OrDefault().ShouldBe(-15); + item.GetInt64OrDefault().ShouldBe(-15); + item.GetDecimalOrDefault().ShouldBe(-15); + item.GetDoubleOrDefault().ShouldBe(-15); + item.GetSingleOrDefault().ShouldBe(-15); + item.GetSByteOrDefault().ShouldBe((sbyte)-15); + + item.GetValueOrDefault().ShouldBe((short)-15); + item.GetValueOrDefault().ShouldBe(-15); + item.GetValueOrDefault().ShouldBe(-15); + item.GetValueOrDefault().ShouldBe(-15); + item.GetValueOrDefault().ShouldBe(-15); + item.GetValueOrDefault().ShouldBe(-15); + item.GetValueOrDefault().ShouldBe((sbyte)-15); + } + + [Fact] + public void WhenUnsignedNumbersRequired_ButSignedUsed_ShouldFail() + { + const string json = """{ "item": -1 }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.GetUInt16OrDefault().ShouldBe(default); + item.GetUInt32OrDefault().ShouldBe(default); + item.GetUInt64OrDefault().ShouldBe(default); + item.GetByteOrDefault().ShouldBe(default); + } + + [Theory] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": "" }""")] + [InlineData("""{ "item": [] }""")] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": true }""")] + [InlineData("""{ "item": false }""")] + public void WhenValueIsNotNumber_ShouldFail(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetInt16OrDefault().ShouldBe(default); + item.GetInt32OrDefault().ShouldBe(default); + item.GetInt64OrDefault().ShouldBe(default); + item.GetUInt16OrDefault().ShouldBe(default); + item.GetUInt32OrDefault().ShouldBe(default); + item.GetUInt64OrDefault().ShouldBe(default); + item.GetByteOrDefault().ShouldBe(default); + item.GetSByteOrDefault().ShouldBe(default); + item.GetDecimalOrDefault().ShouldBe(default); + item.GetDoubleOrDefault().ShouldBe(default); + item.GetSingleOrDefault().ShouldBe(default); + + item.TryGetValue(out short _).ShouldBeFalse(); + item.TryGetValue(out int _).ShouldBeFalse(); + item.TryGetValue(out long _).ShouldBeFalse(); + item.TryGetValue(out uint _).ShouldBeFalse(); + item.TryGetValue(out ushort _).ShouldBeFalse(); + item.TryGetValue(out ulong _).ShouldBeFalse(); + item.TryGetValue(out byte _).ShouldBeFalse(); + item.TryGetValue(out sbyte _).ShouldBeFalse(); + item.TryGetValue(out decimal _).ShouldBeFalse(); + item.TryGetValue(out float _).ShouldBeFalse(); + item.TryGetValue(out double _).ShouldBeFalse(); + } + + [Fact] + public void WhenFloatsUsed_ShouldSucceed() + { + const string json = """{ "item": 2021.1025 }"""; + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetDecimalOrDefault().ShouldBe(2021.1025M); + item.GetDoubleOrDefault().ShouldBe(2021.1025D); + item.GetSingleOrDefault().ShouldBe((float)2021.1025); + + item.GetValueOrDefault().ShouldBe(2021.1025M); + item.GetValueOrDefault().ShouldBe((float)2021.1025M); + item.GetValueOrDefault().ShouldBe(2021.1025D); + } + + [Fact] + public void WhenValidByteUsed_ShouldSucceed() + { + const string json = """{ "item": 255 }"""; + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetByteOrDefault().ShouldBe((byte)255); + item.GetValueOrDefault().ShouldBe((byte)255); + } + + [Fact] + public void WhenInvalidByteValueUsed_ShouldSucceed() + { + const string json = """{ "item": 256 }"""; + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetByteOrDefault().ShouldBe((byte)0); + item.TryGetValue(out byte _).ShouldBeFalse(); + } +} \ No newline at end of file diff --git a/Unit Tests/PropertyTests.cs b/Unit Tests/PropertyTests.cs new file mode 100644 index 0000000..350af84 --- /dev/null +++ b/Unit Tests/PropertyTests.cs @@ -0,0 +1,281 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class PropertyTests +{ + private const string Json = """{ "item1" : "first", "item2": "second" }"""; + private const string Json2 = """{ "item1" : "first", "item2": "second", "item3": "third" }"""; + private static readonly string[] Expected = ["item1", "item2"]; + + [Fact] + public void JsonElementKind_ShouldBeObject() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + } + + [Fact] + public void WhenInvalidElementKind_ShouldFail() + { + const string json = """{ "item1" : "first" }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]; + item.Values.ShouldBeEmpty(); + } + + [Fact] + public void WhenPropertyExist_ValueShouldNotBeNull() + { + const string json = """{ "item1" : "value" }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]; + item.Exist.ShouldBeTrue(); + item.IsNullValue.ShouldBeFalse(); + } + + [Fact] + public void WhenPropertyIsNull_ShouldSucceed() + { + const string json = """{ "item1" : null }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]; + item.Exist.ShouldBeTrue(); + item.IsNullValue.ShouldBeTrue(); + } + + [Fact] + public void WhenPropertyIsEmptyObject_ShouldSucceed() + { + const string json = """{ "item1" : {} }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]; + item.Exist.ShouldBeTrue(); + item.IsNullValue.ShouldBeFalse(); + item.Count.ShouldBe(0); + } + + [Fact] + public void WhenPropertyDoesNotExist_ValueShouldBeNull() + { + const string json = """{ "item1" : null }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item2"]; + item.Exist.ShouldBeFalse(); + item.IsNullValue.ShouldBeTrue(); + } + + [Fact] + public void JsonPropertiesCount_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav.Count.ShouldBe(2); + } + + [Fact] + public void JsonPropertiesNames_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav.Keys.Count().ShouldBe(2); + nav.Keys.ShouldBeSubsetOf(["item1", "item2"]); + } + + [Fact] + public void GetNonExistingProperty_ShouldFail() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav["item3"].Exist.ShouldBeFalse(); + } + + [Fact] + public void GetNonExistingPropertyByIndex_ShouldFail() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav[nav.Count].Exist.ShouldBeFalse(); + } + + [Fact] + public void WhenIndexOutOfRange_ShouldFail() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + Should.Throw(() => nav[-1].Exist); + } + + [Fact] + public void JsonPropertiesValues_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + nav.Values.Count().ShouldBe(2); + nav.Values.ShouldAllBe(x => x.Exist); + nav.Values.ShouldAllBe(x => x.JsonElement.ValueKind == JsonValueKind.String); + nav.Values.Select(x => x.GetStringOrEmpty()).ShouldBeSubsetOf(["first", "second"]); + } + + [Fact] + public void JsonPropertiesTryGetValue_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation(); + + foreach (var navKey in nav.Keys) + { + nav.TryGetValue(navKey, out var item).ShouldBeTrue(); + nav.Values.ShouldContain(item); + } + } + + [Fact] + public void AsDictionary_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation(); + + var dict = (IReadOnlyDictionary) nav; + foreach (var pairs in dict) + { + nav[pairs.Key].Exist.ShouldBeTrue(); + nav.Keys.ShouldContain(pairs.Key); + nav.Values.ShouldContain(pairs.Value); + } + } + + [Fact] + public void AsList_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation(); + + var list = (IReadOnlyList) nav; + foreach (var elem in list) + { + elem.Exist.ShouldBeTrue(); + nav.Keys.ShouldContain(elem.Name); + nav[elem.Name].Exist.ShouldBeTrue(); + } + } + + [Fact] + public void WhenHierarchyLevel0_ShouldExist() + { + const string json = """{ "item1" : 123, "item2": "item2_value" }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var first = nav["item1"]; + first.Exist.ShouldBeTrue(); + first.GetInt32OrDefault().ShouldBe(123); + + var second = nav["item2"]; + second.Exist.ShouldBeTrue(); + second.GetStringOrEmpty().ShouldBe("item2_value"); + } + + [Fact] + public void WhenHierarchyLevel1_ShouldExist() + { + const string json = """{ "item1" : { "item2" : 123 } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]["item2"]; + item.Exist.ShouldBeTrue(); + item.GetInt32OrDefault().ShouldBe(123); + } + + [Fact] + public void WhenHierarchyLevel2_ShouldExist() + { + const string json = """{ "item1" : { "item2" : { "item3" : "test-string" } } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]["item2"]["item3"]; + item.Exist.ShouldBeTrue(); + item.GetStringOrEmpty().ShouldBe("test-string"); + } + + [Fact] + public void WhenHierarchyLevel0_ShouldNotExist() + { + const string json = """{ "item1" : 123}"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item2"]; + item.Exist.ShouldBeFalse(); + } + + [Fact] + public void WhenHierarchyLevel1_ShouldNotExist() + { + const string json = """{ "item1" : { "item2" : 123 } }"""; + + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item1"]["item3"]; + item.Exist.ShouldBeFalse(); + } + + [Fact] + public void EnumerateObject_ShouldEnumerateProperties() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation(); + + var list = new List(); + foreach (var item in nav) + { + list.Add(item.Name); + } + list.ShouldBe(Expected); + } + + [Fact] + public void EnumerateObject_CachedProperties_ShouldEnumerateProperties() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithCachedProperties(); + + var list = new List(); + foreach (var item in nav) + { + list.Add(item.Name); + } + list.ShouldBe(Expected); + } +} \ No newline at end of file diff --git a/Unit Tests/StablePropertyOrderAndCachedPropertiesTests.cs b/Unit Tests/StablePropertyOrderAndCachedPropertiesTests.cs new file mode 100644 index 0000000..c2857f0 --- /dev/null +++ b/Unit Tests/StablePropertyOrderAndCachedPropertiesTests.cs @@ -0,0 +1,95 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class StablePropertyOrderAndCachedPropertiesTests +{ + private const string Json = """{ "item3": 3, "item1": { "item12" : 12, "item10" : 10, "item11" : 11 }, "item4": 4, "item0" : 0 }"""; + private const string Json2 = """{ "item3": 3, "item1": 1, "item40": 40, "item0" : 0 }"""; + private const string Json3 = """{ "item3": 3, "item1": 1, "item4": 4, "item0" : 0 }"""; + + [Fact] + public void WithBoth_PropertiesShouldBeOrdered() + { + var jsonDocument = JsonDocument.Parse(Json3); + var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); + + var first = nav[0]; + first.Exist.ShouldBeTrue(); + first.Name.ShouldBe("item0"); + first.GetInt32OrDefault().ShouldBe(0); + + nav[1].GetInt32OrDefault().ShouldBe(1); + nav[2].GetInt32OrDefault().ShouldBe(3); + nav[3].GetInt32OrDefault().ShouldBe(4); + } + + [Fact] + public void WithBothAndDescendants_PropertiesShouldBeOrdered() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); + + var first = nav[1]; + first.Exist.ShouldBeTrue(); + first.Name.ShouldBe("item1"); + first.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + var inner = first[2]; + inner.Exist.ShouldBeTrue(); + inner.Name.ShouldBe("item12"); + inner.GetInt32OrDefault().ShouldBe(12); + } + + [Fact] + public void WithBothAndDescendantsAndSameObject_PropertiesShouldBeOrdered() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); + + var item = nav[1]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item1"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + item[0].GetInt32OrDefault().ShouldBe(10); + item[1].GetInt32OrDefault().ShouldBe(11); + item[2].GetInt32OrDefault().ShouldBe(12); + } + + [Fact] + public void WithBothAndDescendantsAndSameObject_ThenWithoutCachedProperties_PropertiesShouldBeOrdered() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation() + .WithCachedProperties() + .WithStablePropertyOrder() + .WithoutCachedProperties(); + + var item = nav[1]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item1"); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + item[0].GetInt32OrDefault().ShouldBe(10); + item[1].GetInt32OrDefault().ShouldBe(11); + item[2].GetInt32OrDefault().ShouldBe(12); + } + + [Fact] + public void WithBoth_AndThenWithoutBoth_PropertiesShouldBeOrdered() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation() + .WithCachedProperties() + .WithStablePropertyOrder() + .WithoutCachedProperties() + .WithoutStablePropertyOrder(); + + nav["item0"].GetInt32OrDefault().ShouldBe(0); + nav["item1"].GetInt32OrDefault().ShouldBe(1); + nav["item3"].GetInt32OrDefault().ShouldBe(3); + nav["item40"].GetInt32OrDefault().ShouldBe(40); + } +} \ No newline at end of file diff --git a/Unit Tests/StablePropertyOrderTests.cs b/Unit Tests/StablePropertyOrderTests.cs new file mode 100644 index 0000000..5f9e8ef --- /dev/null +++ b/Unit Tests/StablePropertyOrderTests.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class StablePropertyOrderTests +{ + private const string Json = """{ "item3": 3, "item1": 1, "item4": 4}"""; + private const string Json2 = """{ "item11" : 11, "item10": { "item21" : 21, "item20" : 20, "item22" : 22 } }"""; + + [Fact] + public void WithStablePropertyOrder_ShouldBeSorted() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithStablePropertyOrder()[0]; + + nav.Exist.ShouldBeTrue(); + nav.HasName.ShouldBeTrue(); + nav.Name.ShouldBe("item1"); + } + + [Fact] + public void WithStablePropertyOrderMultipleAccess_ShouldBeSorted() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); + + var nav1 = nav[0]; + var nav2 = nav[1]; + var nav3 = nav[2]; + + nav1.Exist.ShouldBeTrue(); + nav1.HasName.ShouldBeTrue(); + nav1.Name.ShouldBe("item1"); + + nav2.Exist.ShouldBeTrue(); + nav2.HasName.ShouldBeTrue(); + nav2.Name.ShouldBe("item3"); + + nav3.Exist.ShouldBeTrue(); + nav3.HasName.ShouldBeTrue(); + nav3.Name.ShouldBe("item4"); + } + + [Fact] + public void WithStableDescendants_ShouldAlsoBeSorted() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); + + var first = nav[0]; + var innerFirst = first[0]; + + first.Exist.ShouldBeTrue(); + first.HasName.ShouldBeTrue(); + first.Name.ShouldBe("item10"); + first.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); + + innerFirst.Exist.ShouldBeTrue(); + innerFirst.HasName.ShouldBeTrue(); + innerFirst.Name.ShouldBe("item20"); + innerFirst.GetInt32OrDefault().ShouldBe(20); + } + + [Fact] + public void WithStableDescendantsAndSameObject_ShouldAlsoBeSorted() + { + var jsonDocument = JsonDocument.Parse(Json2); + var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); + + var item = nav[0]; + item.Exist.ShouldBeTrue(); + item.HasName.ShouldBeTrue(); + item.Name.ShouldBe("item10"); + + item[0].GetInt32OrDefault().ShouldBe(20); + item[1].GetInt32OrDefault().ShouldBe(21); + item[2].GetInt32OrDefault().ShouldBe(22); + } + + [Fact] + public void WithoutStableDescendants_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithoutStablePropertyOrder(); + + var item = nav["item4"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item4"); + item.GetInt32OrDefault().ShouldBe(4); + } + + [Fact] + public void WithThenWithoutStableDescendants_ShouldSucceed() + { + var jsonDocument = JsonDocument.Parse(Json); + var nav = jsonDocument.ToNavigation().WithStablePropertyOrder().WithoutStablePropertyOrder(); + + var item = nav["item3"]; + item.Exist.ShouldBeTrue(); + item.Name.ShouldBe("item3"); + item.GetInt32OrDefault().ShouldBe(3); + } +} \ No newline at end of file diff --git a/Unit Tests/ValueKindsTests.cs b/Unit Tests/ValueKindsTests.cs new file mode 100644 index 0000000..fcbfe60 --- /dev/null +++ b/Unit Tests/ValueKindsTests.cs @@ -0,0 +1,213 @@ +using System.Text; +using System.Text.Json; +using Shouldly; + +namespace JsonEasyNavigation.Tests; + +public class OtherTypesTests +{ + [Theory] + [InlineData("""{ "item": true }""", JsonValueKind.True, true)] + [InlineData("""{ "item": false }""", JsonValueKind.False, false)] + public void WhenValueIsBoolean_ShouldSucceed(string json, JsonValueKind kind, bool expected) + { + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.JsonElement.ValueKind.ShouldBe(kind); + item.GetBooleanOrDefault().ShouldBe(expected); + item.TryGetValue(out bool value).ShouldBeTrue(); + value.ShouldBe(expected); + } + + [Theory] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": [] }""")] + [InlineData("""{ "item": "string" }""")] + [InlineData("""{ "item": 0 }""")] + [InlineData("""{ "item": 0.1 }""")] + public void WhenValueIsInvalidBoolean_ShouldFail(string json) + { + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.True); + item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.False); + item.GetBooleanOrDefault().ShouldBeFalse(); + item.TryGetValue(out bool _).ShouldBeFalse(); + } + + [Fact] + public void WhenValueIsDateTime_ShouldSucceed() + { + const string json = """{ "item": "2021-10-25T12:30:30" }"""; + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetDateTimeOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); + item.GetValueOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); + item.GetDateTimeOffsetOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); + item.GetValueOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); + } + + [Fact] + public void WhenValueIsDateTimeOffset_ShouldSucceed() + { + const string json = """{ "item": "2021-10-25T12:30:30+03:00" }"""; + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + var offset = new DateTimeOffset(new DateTime(2021, 10, 25, 12, 30, 30, DateTimeKind.Unspecified), TimeSpan.FromHours(3)); + item.GetDateTimeOffsetOrDefault().ShouldBe(offset); + item.GetValueOrDefault().ShouldBe(offset); + } + + [Theory] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": "string" }""")] + [InlineData("""{ "item": 0 }""")] + [InlineData("""{ "item": 0.1 }""")] + [InlineData("""{ "item": true }""")] + [InlineData("""{ "item": false }""")] + [InlineData("""{ "item": [] }""")] + public void WhenValueIsInvalidDateTime_ShouldSucceed(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetDateTimeOffsetOrDefault().ShouldBe(default); + item.TryGetValue(out DateTimeOffset _).ShouldBe(default); + } + + [Fact] + public void WhenValueIsGuid_ShouldSucceed() + { + var guid = Guid.NewGuid(); + var json = $$"""{ "item": "{{guid.ToString()}}" }"""; + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetGuidOrDefault().ShouldBe(guid); + item.GetValueOrDefault().ShouldBe(guid); + } + + [Theory] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": "string" }""")] + [InlineData("""{ "item": 0 }""")] + [InlineData("""{ "item": 0.1 }""")] + [InlineData("""{ "item": true }""")] + [InlineData("""{ "item": false }""")] + [InlineData("""{ "item": [] }""")] + public void WhenValueIsNotGuid_ShouldFail(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetGuidOrDefault().ShouldBe(default); + item.GetValueOrDefault().ShouldBe(default); + } + + [Theory] + [InlineData("""{ "item": "string" }""")] + [InlineData("""{ "item": "\u0073\u0074\u0072\u0069\u006e\u0067" }""")] + public void WhenValueIsString_ShouldSucceed(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.JsonElement.ValueKind.ShouldBe(JsonValueKind.String); + item.GetStringOrEmpty().ShouldBe("string"); + item.GetValueOrDefault().ShouldBe("string"); + } + + [Theory] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": 0 }""")] + [InlineData("""{ "item": 0.1 }""")] + [InlineData("""{ "item": true }""")] + [InlineData("""{ "item": false }""")] + [InlineData("""{ "item": [] }""")] + public void WhenValueIsString_ShouldFail(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.String); + item.GetStringOrDefault().ShouldBe(default); + item.GetStringOrEmpty().ShouldBe(string.Empty); + item.TryGetValue(out string _).ShouldBeFalse(); + } + + [Fact] + public void WhenValueIsBase64_ShouldSucceed() + { + const string json = """{ "item": "SGVsbG8sIHdvcmxkIQ==" }"""; + var jsonDocument = JsonDocument.Parse(json); + + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + + var bytes = item.GetBytesFromBase64OrDefault(); + Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); + + bytes = item.GetValueOrDefault(); + Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); + + using var stream = item.GetStreamFromBase64OrDefault(); + stream.ReadExactly(bytes, 0, (int)stream.Length); + Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); + + using var stream2 = item.GetValueOrDefault(); + stream2.ReadExactly(bytes, 0, (int)stream2.Length); + Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); + } + + [Theory] + [InlineData("""{ "item": null }""")] + [InlineData("""{ "item": {} }""")] + [InlineData("""{ "item": 0 }""")] + [InlineData("""{ "item": 0.1 }""")] + [InlineData("""{ "item": true }""")] + [InlineData("""{ "item": false }""")] + [InlineData("""{ "item": [] }""")] + public void WhenValueIsNotBase64_ShouldFail(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var nav = jsonDocument.ToNavigation(); + + var item = nav["item"]; + item.Exist.ShouldBeTrue(); + item.GetBytesFromBase64OrDefault().ShouldBe(default); + item.TryGetValue(out byte[] _).ShouldBeFalse(); + } +} \ No newline at end of file diff --git a/src/ArrayEnumeratorWrapper.cs b/src/ArrayEnumeratorWrapper.cs new file mode 100644 index 0000000..9c9c8f6 --- /dev/null +++ b/src/ArrayEnumeratorWrapper.cs @@ -0,0 +1,25 @@ +using System.Collections; + +namespace JsonEasyNavigation; + +internal struct ArrayEnumeratorWrapper : IEnumerator +{ + private JsonElement.ArrayEnumerator _enumerator; + + public ArrayEnumeratorWrapper(JsonElement jsonElement) + { + if (jsonElement.ValueKind != JsonValueKind.Array) + throw new InvalidOperationException("JsonElement must be of 'Array' kind"); + _enumerator = jsonElement.EnumerateArray(); + } + + public bool MoveNext() => _enumerator.MoveNext(); + + public void Reset() => _enumerator.Reset(); + + public JsonNavigationElement Current => _enumerator.Current.ToNavigation(); + + object IEnumerator.Current => Current; + + public void Dispose() => _enumerator.Dispose(); +} \ No newline at end of file diff --git a/src/BoxingSafeConverter.cs b/src/BoxingSafeConverter.cs new file mode 100644 index 0000000..c67b1a3 --- /dev/null +++ b/src/BoxingSafeConverter.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +namespace JsonEasyNavigation; + +// https://stackoverflow.com/questions/3343551/how-to-cast-a-value-of-generic-type-t-to-double-without-boxing +internal sealed class BoxingSafeConverter +{ + public static readonly BoxingSafeConverter Instance = new(); + + public Func Convert { get; } + + private BoxingSafeConverter() + { + if (typeof (TIn) != typeof (TOut)) + throw new InvalidOperationException("Both generic type parameters must represent the same type."); + + var paramExpr = Expression.Parameter(typeof (TIn)); + Convert = Expression.Lambda>(paramExpr, paramExpr).Compile(); + } +} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/ArrayTests.cs b/src/JsonEasyNavigation.Tests/ArrayTests.cs deleted file mode 100644 index 4fc54e9..0000000 --- a/src/JsonEasyNavigation.Tests/ArrayTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class ArrayTests - { - [Fact] - public void GetArrayItem_ShouldSucceed() - { - var json = @"[ ""item1"", ""item2"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item1 = nav[0]; - var item2 = nav[1]; - - nav.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); - nav.Count.ShouldBe(2); - - item1.Exist.ShouldBeTrue(); - item1.Index.ShouldBe(0); - item1.GetStringOrEmpty().ShouldBe("item1"); - - item2.Exist.ShouldBeTrue(); - item2.Index.ShouldBe(1); - item2.GetStringOrEmpty().ShouldBe("item2"); - } - - [Fact] - public void WhenEmptyArray_ShouldSucceed() - { - var json = @"[ ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Exist.ShouldBeTrue(); - nav.IsNullValue.ShouldBeFalse(); - nav.Count.ShouldBe(0); - } - - [Fact] - public void ArrayCount_ShouldSucceed() - { - var json = @"[ ""item1"", ""item2"", ""item3"", ""item4"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Count.ShouldBe(4); - } - - [Fact] - public void GetNonExistingArrayItem_ShouldSucceed() - { - var json = @"[ ""item1"", ""item2"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav[nav.Count].Exist.ShouldBeFalse(); - } - - [Fact] - public void WhenIndexOutOfRange_ShouldThrow() - { - var json = @"[ ""item1"", ""item2"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - Should.Throw(() => nav[-1].Exist); - } - - [Fact] - public void WhenArrayInProperty_ShouldSucceed() - { - var json = @"{ ""item1"": 1, ""item2"": 2, ""item3"": [ ""item31"", ""item32"", ""item33"" ] }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item3"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item3"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); - item.Count.ShouldBe(3); - - item[0].GetStringOrEmpty().ShouldBe("item31"); - item[1].GetStringOrEmpty().ShouldBe("item32"); - item[2].GetStringOrEmpty().ShouldBe("item33"); - } - - [Fact] - public void WhenArrayInArray_ShouldSucceed() - { - var json = @"[ ""item1"", ""item2"", [ ""item31"", ""item32"" ] ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav[2]; - item.Exist.ShouldBeTrue(); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Array); - item.Count.ShouldBe(2); - - var inner = item[1]; - inner.Exist.ShouldBeTrue(); - inner.GetStringOrEmpty().ShouldBe("item32"); - } - - [Fact] - public void WhenEnumeratingKey_ShouldBeEmpty() - { - var json = @"[ ""item1"", ""item2"", ""item3"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Keys.ShouldBeEmpty(); - } - - [Fact] - public void EnumerateArray_ShouldEnumerateItems() - { - var json = @"[ ""item1"", ""item2"", ""item3"" ]"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var list = new List(); - foreach (var item in nav) - { - list.Add(item.GetStringOrEmpty()); - } - list.ShouldBe(new[] { "item1", "item2", "item3" }); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/CachedPropertiesTests.cs b/src/JsonEasyNavigation.Tests/CachedPropertiesTests.cs deleted file mode 100644 index 608bd2d..0000000 --- a/src/JsonEasyNavigation.Tests/CachedPropertiesTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class CachedPropertiesTests - { - [Fact] - public void WithCachedProperties_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item1"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item1"); - item.GetInt32OrDefault().ShouldBe(1); - } - - [Fact] - public void WithCachedPropertiesAndDescendants_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": { ""item42"" : 42, ""item41"": 41 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item4"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item4"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - var innerItem = item["item41"]; - innerItem.Exist.ShouldBeTrue(); - innerItem.Name.ShouldBe("item41"); - innerItem.GetInt32OrDefault().ShouldBe(41); - } - - [Fact] - public void WithoutCachedProperties_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": { ""item42"" : 42, ""item41"": 41 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithoutCachedProperties(); - - var item = nav["item4"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item4"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - var innerItem = item["item41"]; - innerItem.Exist.ShouldBeTrue(); - innerItem.Name.ShouldBe("item41"); - innerItem.GetInt32OrDefault().ShouldBe(41); - } - - [Fact] - public void WithThenWithoutCachedProperties_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": { ""item42"" : 42, ""item41"": 41 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties().WithoutCachedProperties(); - - var item = nav["item4"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item4"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - var innerItem = item["item41"]; - innerItem.Exist.ShouldBeTrue(); - innerItem.Name.ShouldBe("item41"); - innerItem.GetInt32OrDefault().ShouldBe(41); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/ExtensionsTests.cs b/src/JsonEasyNavigation.Tests/ExtensionsTests.cs deleted file mode 100644 index 3f5434f..0000000 --- a/src/JsonEasyNavigation.Tests/ExtensionsTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class ExtensionsTests - { - [Fact] - public void UsingPath_ShouldFindProperty() - { - var json = @"{ ""item1"" : { ""item2"" : { ""item3"" : ""test-string"" } } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.RootElement.Property("item1", "item2", "item3"); - - nav.Exist.ShouldBeTrue(); - nav.GetStringOrDefault().ShouldBe("test-string"); - } - - [Fact] - public void UsingPath_ShouldNotFindProperty() - { - var json = @"{ ""item1"" : { ""item2"" : { ""item3"" : ""test-string"" } } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.RootElement.Property("item1", "item2", "item3", "item4"); - - nav.Exist.ShouldBeFalse(); - } - - [Fact] - public void UsingPathAndGetProperty_ShouldFindProperty() - { - var json = @"{ ""item1"" : { ""item2"" : { ""item3"" : { ""item4"" : 100 } } } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.RootElement.Property("item1", "item2", "item3")["item4"]; - - nav.Exist.ShouldBeTrue(); - nav.GetInt32OrDefault().ShouldBe(100); - } - - [Fact] - public void SelectFromManyJsonSources_SumShouldBeCorrect() - { - var json1 = @"{ ""item3"": 3, ""item1"": 10, ""item4"": 4}"; - var json2 = @"{ ""item3"": 3, ""item1"": 15, ""item4"": 4}"; - var json3 = @"{ ""item3"": 3, ""item1"": 99, ""item4"": 4}"; - - var jsonDocuments = new[] - { - JsonDocument.Parse(json1), - JsonDocument.Parse(json2), - JsonDocument.Parse(json3), - }; - - var sum = jsonDocuments - .NavigateSelect(x => x["item1"]) - .Select(x => x.GetInt32OrDefault()) - .Sum(); - - sum.ShouldBe(10 + 15 + 99); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/JsonEasyNavigation.Tests.csproj b/src/JsonEasyNavigation.Tests/JsonEasyNavigation.Tests.csproj deleted file mode 100644 index 74b5fb8..0000000 --- a/src/JsonEasyNavigation.Tests/JsonEasyNavigation.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net8.0 - - false - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/src/JsonEasyNavigation.Tests/MappingTests.cs b/src/JsonEasyNavigation.Tests/MappingTests.cs deleted file mode 100644 index 50f51c3..0000000 --- a/src/JsonEasyNavigation.Tests/MappingTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class MappingTests - { - private class SimpleObject - { - public int item1 { get; set; } - } - - private class HierarchyObject - { - public class InnerObject - { - public decimal inner { get; set; } - } - - public int item1 { get; set; } - public string item2 { get; set; } - public InnerObject item3 { get; set; } - } - - private class HierarchyObjectWithArray - { - public class InnerObject - { - public decimal item1 { get; set; } - public string item2 { get; set; } - public InnerObject[] innerArray { get; set; } - } - - public InnerObject[] array { get; set; } - } - - [Fact] - public void SimpleObjectMapping_ShouldSucceed() - { - var json = @"{""item1"": 2021}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var obj = nav.Map(); - obj.ShouldNotBeNull(); - obj.item1.ShouldBe(2021); - } - - [Fact] - public void HierarchyObjectMapping_ShouldSucceed() - { - var json = @"{""item1"": 2021, ""item2"": ""string"", ""item3"": {""inner"": 123.321} }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var obj = nav.Map(); - obj.ShouldNotBeNull(); - obj.item1.ShouldBe(2021); - obj.item2.ShouldBe("string"); - obj.item3.ShouldNotBeNull(); - obj.item3.inner.ShouldBe(123.321M); - } - - [Fact] - public void HierarchySubObjectMapping_ShouldSucceed() - { - var json = @"{""item1"": 2021, ""item2"": ""string"", ""item3"": {""inner"": 123.321} }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var obj = nav["item3"].Map(); - obj.ShouldNotBeNull(); - obj.inner.ShouldBe(123.321M); - } - - [Fact] - public void HierarchyWithArrayMapping_ShouldSucceed() - { - var json = @"{ ""array"": [ - { ""item1"": 123.321, ""item2"": ""string"", - ""innerArray"": [ - { ""item1"": 999.111, ""item2"": ""anotherString"", ""innerArray"" : [] } - ]} ] }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var obj = nav.Map(); - obj.ShouldNotBeNull(); - obj.array.ShouldNotBeEmpty(); - foreach (var o in obj.array) - { - o.item1.ShouldBe(123.321M); - o.item2.ShouldBe("string"); - o.innerArray.ShouldNotBeEmpty(); - foreach (var innerObject in o.innerArray) - { - innerObject.item1.ShouldBe(999.111M); - innerObject.item2.ShouldBe("anotherString"); - innerObject.innerArray.ShouldBeEmpty(); - } - } - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/NumberTests.cs b/src/JsonEasyNavigation.Tests/NumberTests.cs deleted file mode 100644 index 8169b30..0000000 --- a/src/JsonEasyNavigation.Tests/NumberTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class NumberTests - { - [Fact] - public void WhenCorrectNumber_ShouldSucceed() - { - var json = @"{ ""item"": 42 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetInt16OrDefault().ShouldBe((short)42); - item.GetInt32OrDefault().ShouldBe(42); - item.GetInt64OrDefault().ShouldBe(42); - item.GetUInt16OrDefault().ShouldBe((ushort)42); - item.GetUInt32OrDefault().ShouldBe((uint)42); - item.GetUInt64OrDefault().ShouldBe((ulong)42); - item.GetByteOrDefault().ShouldBe((byte)42); - item.GetDecimalOrDefault().ShouldBe(42); - item.GetDoubleOrDefault().ShouldBe(42); - item.GetSingleOrDefault().ShouldBe(42); - - item.GetValueOrDefault().ShouldBe(42); - item.GetValueOrDefault().ShouldBe((short)42); - item.GetValueOrDefault().ShouldBe(42); - item.GetValueOrDefault().ShouldBe((uint)42); - item.GetValueOrDefault().ShouldBe((ushort)42); - item.GetValueOrDefault().ShouldBe((ulong)42); - item.GetValueOrDefault().ShouldBe((byte)42); - item.GetValueOrDefault().ShouldBe(42); - item.GetValueOrDefault().ShouldBe(42); - item.GetValueOrDefault().ShouldBe(42); - } - - [Fact] - public void WhenSignedNumberUsed_ShouldSucceed() - { - var json = @"{ ""item"": -15 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetInt16OrDefault().ShouldBe((short)-15); - item.GetInt32OrDefault().ShouldBe(-15); - item.GetInt64OrDefault().ShouldBe(-15); - item.GetDecimalOrDefault().ShouldBe(-15); - item.GetDoubleOrDefault().ShouldBe(-15); - item.GetSingleOrDefault().ShouldBe(-15); - item.GetSByteOrDefault().ShouldBe((sbyte)-15); - - item.GetValueOrDefault().ShouldBe((short)-15); - item.GetValueOrDefault().ShouldBe(-15); - item.GetValueOrDefault().ShouldBe(-15); - item.GetValueOrDefault().ShouldBe(-15); - item.GetValueOrDefault().ShouldBe(-15); - item.GetValueOrDefault().ShouldBe(-15); - item.GetValueOrDefault().ShouldBe((sbyte)-15); - } - - [Fact] - public void WhenUnsignedNumbersRequired_ButSignedUsed_ShouldFail() - { - var json = @"{ ""item"": -1 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.GetUInt16OrDefault().ShouldBe(default); - item.GetUInt32OrDefault().ShouldBe(default); - item.GetUInt64OrDefault().ShouldBe(default); - item.GetByteOrDefault().ShouldBe(default); - } - - [Theory] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": """" }")] - [InlineData(@"{ ""item"": [] }")] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": true }")] - [InlineData(@"{ ""item"": false }")] - public void WhenValueIsNotNumber_ShouldFail(string json) - { - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetInt16OrDefault().ShouldBe(default); - item.GetInt32OrDefault().ShouldBe(default); - item.GetInt64OrDefault().ShouldBe(default); - item.GetUInt16OrDefault().ShouldBe(default); - item.GetUInt32OrDefault().ShouldBe(default); - item.GetUInt64OrDefault().ShouldBe(default); - item.GetByteOrDefault().ShouldBe(default); - item.GetSByteOrDefault().ShouldBe(default); - item.GetDecimalOrDefault().ShouldBe(default); - item.GetDoubleOrDefault().ShouldBe(default); - item.GetSingleOrDefault().ShouldBe(default); - - item.TryGetValue(out short _).ShouldBeFalse(); - item.TryGetValue(out int _).ShouldBeFalse(); - item.TryGetValue(out long _).ShouldBeFalse(); - item.TryGetValue(out uint _).ShouldBeFalse(); - item.TryGetValue(out ushort _).ShouldBeFalse(); - item.TryGetValue(out ulong _).ShouldBeFalse(); - item.TryGetValue(out byte _).ShouldBeFalse(); - item.TryGetValue(out sbyte _).ShouldBeFalse(); - item.TryGetValue(out decimal _).ShouldBeFalse(); - item.TryGetValue(out float _).ShouldBeFalse(); - item.TryGetValue(out double _).ShouldBeFalse(); - } - - [Fact] - public void WhenFloatsUsed_ShouldSucceed() - { - var json = @"{ ""item"": 2021.1025 }"; - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetDecimalOrDefault().ShouldBe(2021.1025M); - item.GetDoubleOrDefault().ShouldBe(2021.1025D); - item.GetSingleOrDefault().ShouldBe((float)2021.1025); - - item.GetValueOrDefault().ShouldBe(2021.1025M); - item.GetValueOrDefault().ShouldBe((float)2021.1025M); - item.GetValueOrDefault().ShouldBe(2021.1025D); - } - - [Fact] - public void WhenValidByteUsed_ShouldSucceed() - { - var json = @"{ ""item"": 255 }"; - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetByteOrDefault().ShouldBe((byte)255); - item.GetValueOrDefault().ShouldBe((byte)255); - } - - [Fact] - public void WhenInvalidByteValueUsed_ShouldSucceed() - { - var json = @"{ ""item"": 256 }"; - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetByteOrDefault().ShouldBe((byte)0); - item.TryGetValue(out byte _).ShouldBeFalse(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/PropertyTests.cs b/src/JsonEasyNavigation.Tests/PropertyTests.cs deleted file mode 100644 index 38f89c5..0000000 --- a/src/JsonEasyNavigation.Tests/PropertyTests.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class PropertyTests - { - [Fact] - public void JsonElementKind_ShouldBeObject() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - } - - [Fact] - public void WhenInvalidElementKind_ShouldFail() - { - var json = @"{ ""item1"" : ""first"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]; - item.Values.ShouldBeEmpty(); - } - - [Fact] - public void WhenPropertyExist_ValueShouldNotBeNull() - { - var json = @"{ ""item1"" : ""value"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]; - item.Exist.ShouldBeTrue(); - item.IsNullValue.ShouldBeFalse(); - } - - [Fact] - public void WhenPropertyIsNull_ShouldSucceed() - { - var json = @"{ ""item1"" : null }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]; - item.Exist.ShouldBeTrue(); - item.IsNullValue.ShouldBeTrue(); - } - - [Fact] - public void WhenPropertyIsEmptyObject_ShouldSucceed() - { - var json = @"{ ""item1"" : {} }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]; - item.Exist.ShouldBeTrue(); - item.IsNullValue.ShouldBeFalse(); - item.Count.ShouldBe(0); - } - - [Fact] - public void WhenPropertyDoesNotExist_ValueShouldBeNull() - { - var json = @"{ ""item1"" : null }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item2"]; - item.Exist.ShouldBeFalse(); - item.IsNullValue.ShouldBeTrue(); - } - - [Fact] - public void JsonPropertiesCount_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Count.ShouldBe(2); - } - - [Fact] - public void JsonPropertiesNames_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Keys.Count().ShouldBe(2); - nav.Keys.ShouldBeSubsetOf(new[] {"item1", "item2"}); - } - - [Fact] - public void GetNonExistingProperty_ShouldFail() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav["item3"].Exist.ShouldBeFalse(); - } - - [Fact] - public void GetNonExistingPropertyByIndex_ShouldFail() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav[nav.Count].Exist.ShouldBeFalse(); - } - - [Fact] - public void WhenIndexOutOfRange_ShouldFail() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - Should.Throw(() => nav[-1].Exist); - } - - [Fact] - public void JsonPropertiesValues_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - nav.Values.Count().ShouldBe(2); - nav.Values.ShouldAllBe(x => x.Exist); - nav.Values.ShouldAllBe(x => x.JsonElement.ValueKind == JsonValueKind.String); - nav.Values.Select(x => x.GetStringOrEmpty()).ShouldBeSubsetOf(new[] {"first", "second"}); - } - - [Fact] - public void JsonPropertiesTryGetValue_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"", ""item3"": ""third"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - foreach (var navKey in nav.Keys) - { - nav.TryGetValue(navKey, out var item).ShouldBeTrue(); - nav.Values.ShouldContain(item); - } - } - - [Fact] - public void AsDictionary_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"", ""item3"": ""third"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var dict = (IReadOnlyDictionary) nav; - foreach (var pairs in dict) - { - nav[pairs.Key].Exist.ShouldBeTrue(); - nav.Keys.ShouldContain(pairs.Key); - nav.Values.ShouldContain(pairs.Value); - } - } - - [Fact] - public void AsList_ShouldSucceed() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"", ""item3"": ""third"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var list = (IReadOnlyList) nav; - foreach (var elem in list) - { - elem.Exist.ShouldBeTrue(); - nav.Keys.ShouldContain(elem.Name); - nav[elem.Name].Exist.ShouldBeTrue(); - } - } - - [Fact] - public void WhenHierarchyLevel0_ShouldExist() - { - var json = @"{ ""item1"" : 123, ""item2"": ""item2_value"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var first = nav["item1"]; - first.Exist.ShouldBeTrue(); - first.GetInt32OrDefault().ShouldBe(123); - - var second = nav["item2"]; - second.Exist.ShouldBeTrue(); - second.GetStringOrEmpty().ShouldBe("item2_value"); - } - - [Fact] - public void WhenHierarchyLevel1_ShouldExist() - { - var json = @"{ ""item1"" : { ""item2"" : 123 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]["item2"]; - item.Exist.ShouldBeTrue(); - item.GetInt32OrDefault().ShouldBe(123); - } - - [Fact] - public void WhenHierarchyLevel2_ShouldExist() - { - var json = @"{ ""item1"" : { ""item2"" : { ""item3"" : ""test-string"" } } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]["item2"]["item3"]; - item.Exist.ShouldBeTrue(); - item.GetStringOrEmpty().ShouldBe("test-string"); - } - - [Fact] - public void WhenHierarchyLevel0_ShouldNotExist() - { - var json = @"{ ""item1"" : 123}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item2"]; - item.Exist.ShouldBeFalse(); - } - - [Fact] - public void WhenHierarchyLevel1_ShouldNotExist() - { - var json = @"{ ""item1"" : { ""item2"" : 123 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var item = nav["item1"]["item3"]; - item.Exist.ShouldBeFalse(); - } - - [Fact] - public void EnumerateObject_ShouldEnumerateProperties() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation(); - - var list = new List(); - foreach (var item in nav) - { - list.Add(item.Name); - } - list.ShouldBe(new[] { "item1", "item2" }); - } - - [Fact] - public void EnumerateObject_CachedProperties_ShouldEnumerateProperties() - { - var json = @"{ ""item1"" : ""first"", ""item2"": ""second"" }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties(); - - var list = new List(); - foreach (var item in nav) - { - list.Add(item.Name); - } - list.ShouldBe(new[] { "item1", "item2" }); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/StablePropertyOrderAndCachedPropertiesTests.cs b/src/JsonEasyNavigation.Tests/StablePropertyOrderAndCachedPropertiesTests.cs deleted file mode 100644 index fde6702..0000000 --- a/src/JsonEasyNavigation.Tests/StablePropertyOrderAndCachedPropertiesTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class StablePropertyOrderAndCachedPropertiesTests - { - [Fact] - public void WithBoth_PropertiesShouldBeOrdered() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4, ""item0"" : 0 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); - - var first = nav[0]; - first.Exist.ShouldBeTrue(); - first.Name.ShouldBe("item0"); - first.GetInt32OrDefault().ShouldBe(0); - - nav[1].GetInt32OrDefault().ShouldBe(1); - nav[2].GetInt32OrDefault().ShouldBe(3); - nav[3].GetInt32OrDefault().ShouldBe(4); - } - - [Fact] - public void WithBothAndDescendants_PropertiesShouldBeOrdered() - { - var json = @"{ ""item3"": 3, ""item1"": { ""item12"" : 12, ""item10"" : 10, ""item11"" : 11 }, ""item4"": 4, ""item0"" : 0 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); - - var first = nav[1]; - first.Exist.ShouldBeTrue(); - first.Name.ShouldBe("item1"); - first.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - var inner = first[2]; - inner.Exist.ShouldBeTrue(); - inner.Name.ShouldBe("item12"); - inner.GetInt32OrDefault().ShouldBe(12); - } - - [Fact] - public void WithBothAndDescendantsAndSameObject_PropertiesShouldBeOrdered() - { - var json = @"{ ""item3"": 3, ""item1"": { ""item12"" : 12, ""item10"" : 10, ""item11"" : 11 }, ""item4"": 4, ""item0"" : 0 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithCachedProperties().WithStablePropertyOrder(); - - var item = nav[1]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item1"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - item[0].GetInt32OrDefault().ShouldBe(10); - item[1].GetInt32OrDefault().ShouldBe(11); - item[2].GetInt32OrDefault().ShouldBe(12); - } - - [Fact] - public void WithBothAndDescendantsAndSameObject_ThenWithoutCachedProperties_PropertiesShouldBeOrdered() - { - var json = @"{ ""item3"": 3, ""item1"": { ""item12"" : 12, ""item10"" : 10, ""item11"" : 11 }, ""item4"": 4, ""item0"" : 0 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation() - .WithCachedProperties() - .WithStablePropertyOrder() - .WithoutCachedProperties(); - - var item = nav[1]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item1"); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - item[0].GetInt32OrDefault().ShouldBe(10); - item[1].GetInt32OrDefault().ShouldBe(11); - item[2].GetInt32OrDefault().ShouldBe(12); - } - - [Fact] - public void WithBoth_AndThenWithoutBoth_PropertiesShouldBeOrdered() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item40"": 40, ""item0"" : 0 }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation() - .WithCachedProperties() - .WithStablePropertyOrder() - .WithoutCachedProperties() - .WithoutStablePropertyOrder(); - - nav["item0"].GetInt32OrDefault().ShouldBe(0); - nav["item1"].GetInt32OrDefault().ShouldBe(1); - nav["item3"].GetInt32OrDefault().ShouldBe(3); - nav["item40"].GetInt32OrDefault().ShouldBe(40); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/StablePropertyOrderTests.cs b/src/JsonEasyNavigation.Tests/StablePropertyOrderTests.cs deleted file mode 100644 index 4e645ff..0000000 --- a/src/JsonEasyNavigation.Tests/StablePropertyOrderTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class StablePropertyOrderTests - { - [Fact] - public void WithStablePropertyOrder_ShouldBeSorted() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithStablePropertyOrder()[0]; - - nav.Exist.ShouldBeTrue(); - nav.HasName.ShouldBeTrue(); - nav.Name.ShouldBe("item1"); - } - - [Fact] - public void WithStablePropertyOrderMultipleAccess_ShouldBeSorted() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); - - var nav1 = nav[0]; - var nav2 = nav[1]; - var nav3 = nav[2]; - - nav1.Exist.ShouldBeTrue(); - nav1.HasName.ShouldBeTrue(); - nav1.Name.ShouldBe("item1"); - - nav2.Exist.ShouldBeTrue(); - nav2.HasName.ShouldBeTrue(); - nav2.Name.ShouldBe("item3"); - - nav3.Exist.ShouldBeTrue(); - nav3.HasName.ShouldBeTrue(); - nav3.Name.ShouldBe("item4"); - } - - [Fact] - public void WithStableDescendants_ShouldAlsoBeSorted() - { - var json = @"{ ""item11"" : 11, ""item10"": { ""item21"" : 21, ""item20"" : 20, ""item22"" : 22 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); - - var first = nav[0]; - var innerFirst = first[0]; - - first.Exist.ShouldBeTrue(); - first.HasName.ShouldBeTrue(); - first.Name.ShouldBe("item10"); - first.JsonElement.ValueKind.ShouldBe(JsonValueKind.Object); - - innerFirst.Exist.ShouldBeTrue(); - innerFirst.HasName.ShouldBeTrue(); - innerFirst.Name.ShouldBe("item20"); - innerFirst.GetInt32OrDefault().ShouldBe(20); - } - - [Fact] - public void WithStableDescendantsAndSameObject_ShouldAlsoBeSorted() - { - var json = @"{ ""item11"" : 11, ""item10"": { ""item21"" : 21, ""item20"" : 20, ""item22"" : 22 } }"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithStablePropertyOrder(); - - var item = nav[0]; - item.Exist.ShouldBeTrue(); - item.HasName.ShouldBeTrue(); - item.Name.ShouldBe("item10"); - - item[0].GetInt32OrDefault().ShouldBe(20); - item[1].GetInt32OrDefault().ShouldBe(21); - item[2].GetInt32OrDefault().ShouldBe(22); - } - - [Fact] - public void WithoutStableDescendants_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithoutStablePropertyOrder(); - - var item = nav["item4"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item4"); - item.GetInt32OrDefault().ShouldBe(4); - } - - [Fact] - public void WithThenWithoutStableDescendants_ShouldSucceed() - { - var json = @"{ ""item3"": 3, ""item1"": 1, ""item4"": 4}"; - - var jsonDocument = JsonDocument.Parse(json); - var nav = jsonDocument.ToNavigation().WithStablePropertyOrder().WithoutStablePropertyOrder(); - - var item = nav["item3"]; - item.Exist.ShouldBeTrue(); - item.Name.ShouldBe("item3"); - item.GetInt32OrDefault().ShouldBe(3); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.Tests/ValueKindsTests.cs b/src/JsonEasyNavigation.Tests/ValueKindsTests.cs deleted file mode 100644 index 5773bf6..0000000 --- a/src/JsonEasyNavigation.Tests/ValueKindsTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Text.Json; -using Shouldly; -using Xunit; - -namespace JsonEasyNavigation.Tests -{ - public class OtherTypesTests - { - [Theory] - [InlineData(@"{ ""item"": true }", JsonValueKind.True, true)] - [InlineData(@"{ ""item"": false }", JsonValueKind.False, false)] - public void WhenValueIsBoolean_ShouldSucceed(string json, JsonValueKind kind, bool expected) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.JsonElement.ValueKind.ShouldBe(kind); - item.GetBooleanOrDefault().ShouldBe(expected); - item.TryGetValue(out bool value).ShouldBeTrue(); - value.ShouldBe(expected); - } - - [Theory] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": [] }")] - [InlineData(@"{ ""item"": ""string"" }")] - [InlineData(@"{ ""item"": 0 }")] - [InlineData(@"{ ""item"": 0.1 }")] - public void WhenValueIsInvalidBoolean_ShouldFail(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.True); - item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.False); - item.GetBooleanOrDefault().ShouldBeFalse(); - item.TryGetValue(out bool _).ShouldBeFalse(); - } - - [Fact] - public void WhenValueIsDateTime_ShouldSucceed() - { - var json = @"{ ""item"": ""2021-10-25T12:30:30"" }"; - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetDateTimeOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); - item.GetValueOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); - item.GetDateTimeOffsetOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); - item.GetValueOrDefault().ShouldBe(new DateTime(2021, 10, 25, 12, 30, 30)); - } - - [Fact] - public void WhenValueIsDateTimeOffset_ShouldSucceed() - { - var json = @"{ ""item"": ""2021-10-25T12:30:30+03:00"" }"; - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - var offset = new DateTimeOffset(new DateTime(2021, 10, 25, 12, 30, 30, DateTimeKind.Unspecified), - TimeSpan.FromHours(3)); - item.GetDateTimeOffsetOrDefault().ShouldBe(offset); - item.GetValueOrDefault().ShouldBe(offset); - } - - [Theory] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": ""string"" }")] - [InlineData(@"{ ""item"": 0 }")] - [InlineData(@"{ ""item"": 0.1 }")] - [InlineData(@"{ ""item"": true }")] - [InlineData(@"{ ""item"": false }")] - [InlineData(@"{ ""item"": [] }")] - public void WhenValueIsInvalidDateTime_ShouldSucceed(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetDateTimeOffsetOrDefault().ShouldBe(default); - item.TryGetValue(out DateTimeOffset _).ShouldBe(default); - } - - [Fact] - public void WhenValueIsGuid_ShouldSucceed() - { - var guid = Guid.NewGuid(); - var json = $@"{{ ""item"": ""{guid.ToString()}"" }}"; - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetGuidOrDefault().ShouldBe(guid); - item.GetValueOrDefault().ShouldBe(guid); - } - - [Theory] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": ""string"" }")] - [InlineData(@"{ ""item"": 0 }")] - [InlineData(@"{ ""item"": 0.1 }")] - [InlineData(@"{ ""item"": true }")] - [InlineData(@"{ ""item"": false }")] - [InlineData(@"{ ""item"": [] }")] - public void WhenValueIsNotGuid_ShouldFail(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetGuidOrDefault().ShouldBe(default); - item.GetValueOrDefault().ShouldBe(default); - } - - [Theory] - [InlineData(@"{ ""item"": ""string"" }")] - [InlineData(@"{ ""item"": ""\u0073\u0074\u0072\u0069\u006e\u0067"" }")] - public void WhenValueIsString_ShouldSucceed(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.JsonElement.ValueKind.ShouldBe(JsonValueKind.String); - item.GetStringOrEmpty().ShouldBe("string"); - item.GetValueOrDefault().ShouldBe("string"); - } - - [Theory] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": 0 }")] - [InlineData(@"{ ""item"": 0.1 }")] - [InlineData(@"{ ""item"": true }")] - [InlineData(@"{ ""item"": false }")] - [InlineData(@"{ ""item"": [] }")] - public void WhenValueIsString_ShouldFail(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.JsonElement.ValueKind.ShouldNotBe(JsonValueKind.String); - item.GetStringOrDefault().ShouldBe(default); - item.GetStringOrEmpty().ShouldBe(string.Empty); - item.TryGetValue(out string _).ShouldBeFalse(); - } - - [Fact] - public void WhenValueIsBase64_ShouldSucceed() - { - var json = @"{ ""item"": ""SGVsbG8sIHdvcmxkIQ=="" }"; - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - - var bytes = item.GetBytesFromBase64OrDefault(); - Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); - - bytes = item.GetValueOrDefault(); - Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); - - using var stream = item.GetStreamFromBase64OrDefault(); - stream.Read(bytes, 0, (int)stream.Length); - Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); - - using var stream2 = item.GetValueOrDefault(); - stream2.Read(bytes, 0, (int)stream2.Length); - Encoding.UTF8.GetString(bytes).ShouldBe("Hello, world!"); - } - - [Theory] - [InlineData(@"{ ""item"": null }")] - [InlineData(@"{ ""item"": {} }")] - [InlineData(@"{ ""item"": 0 }")] - [InlineData(@"{ ""item"": 0.1 }")] - [InlineData(@"{ ""item"": true }")] - [InlineData(@"{ ""item"": false }")] - [InlineData(@"{ ""item"": [] }")] - public void WhenValueIsNotBase64_ShouldFail(string json) - { - var jsonDocument = JsonDocument.Parse(json); - - var nav = jsonDocument.ToNavigation(); - - var item = nav["item"]; - item.Exist.ShouldBeTrue(); - item.GetBytesFromBase64OrDefault().ShouldBe(default); - item.TryGetValue(out byte[] _).ShouldBeFalse(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation.csproj b/src/JsonEasyNavigation.csproj new file mode 100644 index 0000000..78d3e6e --- /dev/null +++ b/src/JsonEasyNavigation.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0;netstandard2.1;net9.0 + 1.3.2 + This library provides a wrapper class around JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. + text.json json navigation indexer dictionary list collection DOM + Update target framework version for .NET 9.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/JsonEasyNavigation.sln b/src/JsonEasyNavigation.sln deleted file mode 100644 index a579300..0000000 --- a/src/JsonEasyNavigation.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonEasyNavigation", "JsonEasyNavigation\JsonEasyNavigation.csproj", "{EFB42F15-6C46-4C1A-94D0-177516E8FDA7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonEasyNavigation.Tests", "JsonEasyNavigation.Tests\JsonEasyNavigation.Tests.csproj", "{5F8113CF-1160-4D4B-8E9A-20D36CD79F04}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EFB42F15-6C46-4C1A-94D0-177516E8FDA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFB42F15-6C46-4C1A-94D0-177516E8FDA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFB42F15-6C46-4C1A-94D0-177516E8FDA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFB42F15-6C46-4C1A-94D0-177516E8FDA7}.Release|Any CPU.Build.0 = Release|Any CPU - {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5F8113CF-1160-4D4B-8E9A-20D36CD79F04}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/src/JsonEasyNavigation/ArrayEnumeratorWrapper.cs b/src/JsonEasyNavigation/ArrayEnumeratorWrapper.cs deleted file mode 100644 index 6eb5dfc..0000000 --- a/src/JsonEasyNavigation/ArrayEnumeratorWrapper.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - internal struct ArrayEnumeratorWrapper : IEnumerator - { - private JsonElement.ArrayEnumerator _enumerator; - - public ArrayEnumeratorWrapper(JsonElement jsonElement) - { - if (jsonElement.ValueKind != JsonValueKind.Array) - throw new InvalidOperationException("JsonElement must be of 'Array' kind"); - _enumerator = jsonElement.EnumerateArray(); - } - - public bool MoveNext() - { - return _enumerator.MoveNext(); - } - - public void Reset() - { - _enumerator.Reset(); - } - - public JsonNavigationElement Current => _enumerator.Current.ToNavigation(); - - object IEnumerator.Current => Current; - - public void Dispose() - { - _enumerator.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/BoxingSafeConverter.cs b/src/JsonEasyNavigation/BoxingSafeConverter.cs deleted file mode 100644 index b5b8527..0000000 --- a/src/JsonEasyNavigation/BoxingSafeConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace JsonEasyNavigation -{ - // https://stackoverflow.com/questions/3343551/how-to-cast-a-value-of-generic-type-t-to-double-without-boxing - internal sealed class BoxingSafeConverter - { - public static readonly BoxingSafeConverter Instance = new BoxingSafeConverter(); - - public Func Convert { get; } - - private BoxingSafeConverter() - { - if (typeof (TIn) != typeof (TOut)) - { - throw new InvalidOperationException("Both generic type parameters must represent the same type."); - } - var paramExpr = Expression.Parameter(typeof (TIn)); - Convert = - Expression.Lambda>(paramExpr, - paramExpr) - .Compile(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/JsonEasyNavigation.csproj b/src/JsonEasyNavigation/JsonEasyNavigation.csproj deleted file mode 100644 index 884fc1a..0000000 --- a/src/JsonEasyNavigation/JsonEasyNavigation.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net5.0;netstandard2.0 - True - 1.3.0 - logo.png - JsonEasyNavigation - Sharkadi Andrey - This library provides a wrapper class around JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. - (c) Sharkadi Andrey - https://github.com/sharkadi-a/JsonEasyNavigation - text.json json navigation indexer dictionary list collection DOM - - - Apache-2.0 - - - - - - - - - - - - - - - - - - - - bin\Debug\JsonEasyNavigation.xml - - - - bin\Release\JsonEasyNavigation.xml - - diff --git a/src/JsonEasyNavigation/JsonExtensions.cs b/src/JsonEasyNavigation/JsonExtensions.cs deleted file mode 100644 index dde1c53..0000000 --- a/src/JsonEasyNavigation/JsonExtensions.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - /// - /// Various , and extensions - /// and utilities. - /// - public static class JsonExtensions - { - /// - /// Makes the and all it's descendants to have a stable order of the - /// properties in object-kind elements. - /// - public static JsonNavigationElement WithStablePropertyOrder(this JsonNavigationElement jsonNavigationElement) - { - return jsonNavigationElement.IsStablePropertyOrder - ? jsonNavigationElement - : new JsonNavigationElement(jsonNavigationElement.JsonElement, true, jsonNavigationElement.HasCachedProperties); - } - - /// - /// Makes the and all it's descendants to have an unstable order (well, - /// may be) of the properties in object-kind elements. This does not imply that elements will definitely have - /// random order, but the order is not guaranteed. - /// - public static JsonNavigationElement WithoutStablePropertyOrder(this JsonNavigationElement jsonNavigationElement) - { - return jsonNavigationElement.IsStablePropertyOrder - ? new JsonNavigationElement(jsonNavigationElement.JsonElement, false, jsonNavigationElement.HasCachedProperties) - : jsonNavigationElement; - } - - /// - /// Makes the and all it's descendants to have properties cached in an array. - /// May be useful if is being used multiple time to access it's - /// properties. Allocates memory in heap (just once, when enumeration is executed). - /// - public static JsonNavigationElement WithCachedProperties(this JsonNavigationElement jsonNavigationElement) - { - return jsonNavigationElement.HasCachedProperties - ? jsonNavigationElement - : new JsonNavigationElement(jsonNavigationElement.JsonElement, - jsonNavigationElement.IsStablePropertyOrder, true); - } - - /// - /// Makes the and all it's descendants to have all properties not to - /// be cached. - /// - /// - /// - public static JsonNavigationElement WithoutCachedProperties(this JsonNavigationElement jsonNavigationElement) - { - return jsonNavigationElement.HasCachedProperties - ? new JsonNavigationElement(jsonNavigationElement.JsonElement, - jsonNavigationElement.IsStablePropertyOrder, false) - : jsonNavigationElement; - } - - /// - /// Creates a wrapper from , which allows to - /// navigate through it's properties and array items like using dictionary or list. - /// - public static JsonNavigationElement ToNavigation(this JsonElement jsonElement) - { - return new JsonNavigationElement(jsonElement, false, false); - } - - /// - /// Creates a wrapper from , which allows to - /// navigate through it's properties and array items like using dictionary or list. - /// - public static JsonNavigationElement ToNavigation(this JsonDocument jsonDocument) - { - return new JsonNavigationElement(jsonDocument.RootElement, false, false); - } - - /// - /// Allows to navigate in-depth through properties by their names. - /// - public static JsonNavigationElement Property(this JsonElement jsonElement, params string[] propertyPath) - { - return PropertyPathVisitor.Visit(jsonElement, new ArraySegment(propertyPath)); - } - - /// - /// Allows to navigate in-depth through properties by their names. - /// - public static JsonNavigationElement Property(this JsonDocument jsonDocument, params string[] propertyPath) - { - return jsonDocument.RootElement.Property(propertyPath); - } - - /// - /// Apply selector to all the one-by-one and return an enumerable with - /// resulting . - /// - public static IEnumerable NavigateSelect(this IEnumerable jsonDocuments, - Func selector) - { - return jsonDocuments.Select(x => x.RootElement).NavigateSelect(selector); - } - - /// - /// Apply selector to all the one-by-one and return an enumerable with - /// resulting . - /// - public static IEnumerable NavigateSelect(this IEnumerable jsonElements, - Func selector) - { - return jsonElements.Select(element => selector(element.ToNavigation())); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyDictionary.cs b/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyDictionary.cs deleted file mode 100644 index 0f3a6c9..0000000 --- a/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyDictionary.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - public readonly partial struct JsonNavigationElement : IReadOnlyDictionary - { - /// - /// Returns a property of having a given name. If there is no such property or - /// is not an object at all, and empty - /// will be returned (and it's Exist property will be false). - /// - /// - /// Say, there is JSON document:
- /// {
- /// "item1": 1,
- /// "item2": 2
- /// }
- /// And a navigation object:
- /// var nav = JsonDocument.Parse(...).ToNavigation();
- ///
- /// We can get property using the indexer of :
- /// var number = nav["item1"].TryGetInt32OrDefault();
- /// The number is 1, as expected. - ///
- public JsonNavigationElement this[string property] - { - get - { - if (JsonElement.ValueKind != JsonValueKind.Object) - { - return default; - } - - return JsonElement.TryGetProperty(property, out var p) - ? new JsonNavigationElement(p, property, IsStablePropertyOrder, HasCachedProperties) - : default; - } - } - - /// - /// A total number of array items or property count in the or 0 for other kind of elements. - /// - public int Count - { - get - { - if (JsonElement.ValueKind == JsonValueKind.Array) - { - return JsonElement.GetArrayLength(); - } - - if (JsonElement.ValueKind == JsonValueKind.Object) - { - return HasCachedProperties ? _properties.Value.Length : JsonElement.EnumerateObject().Count(); - } - - return default; - } - } - - /// - IEnumerator> IEnumerable>.GetEnumerator() - { - if (JsonElement.ValueKind != JsonValueKind.Object) - return Enumerable.Empty>().GetEnumerator(); - - return HasCachedProperties - ? new ObjectEnumeratorWrapper(this, _properties.Value) - : new ObjectEnumeratorWrapper(this, IsStablePropertyOrder); - } - - /// - /// Returns true if the is an object and contains a property with a give name. - /// - public bool ContainsKey(string key) - { - return this[key].Exist; - } - - /// - /// Returns true and a if the contains a property - /// of a given name. - /// - public bool TryGetValue(string key, out JsonNavigationElement value) - { - value = this[key]; - return value.Exist; - } - - /// - /// Returns all property names of the . - /// - public IEnumerable Keys - { - get - { - if (JsonElement.ValueKind != JsonValueKind.Object) - { - return Enumerable.Empty(); - } - - return HasCachedProperties - ? _properties.Value.Select(x => x.Name) - : JsonElement.EnumerateObject().Select(x => x.Name); - } - } - - /// - /// Returns all properties or array items of the or empty enumerable. - /// - public IEnumerable Values - { - get - { - var isStable = IsStablePropertyOrder; - var preCache = HasCachedProperties; - - if (JsonElement.ValueKind == JsonValueKind.Array) - { - return JsonElement.EnumerateArray().Select(x => new JsonNavigationElement(x, isStable, preCache)); - } - - if (JsonElement.ValueKind == JsonValueKind.Object) - { - IEnumerable properties = HasCachedProperties - ? _properties.Value - : (IEnumerable)JsonElement.EnumerateObject(); - return properties.Select(x => new JsonNavigationElement(x.Value, isStable, preCache)); - } - - return Enumerable.Empty(); - } - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyList.cs b/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyList.cs deleted file mode 100644 index 3274ec1..0000000 --- a/src/JsonEasyNavigation/JsonNavigationElement.ReadOnlyList.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - public readonly partial struct JsonNavigationElement : IReadOnlyList - { - /// - /// Returns a of the at required position. This - /// index works for both arrays and objects (the latter returns properties). Keep in mind, that the order of - /// properties in an object is not guaranteed. You should use - /// to persist the order of properties (sorted by their names). - /// - /// - /// Say, there is JSON document:
- /// {
- /// "item1": 1,
- /// "item2": 2
- /// }
- /// And a navigation object:
- /// var nav = JsonDocument.Parse(...).ToNavigation();
- ///
- /// We can get property using the indexer of :
- /// var number = nav[0].TryGetInt32OrDefault();
- /// The number should be 1.
- ///
- /// To be sure, that properties have predicted order, we can use method :
- /// var number = nav.WithStablePropertyOrder()[0];
- /// The number value is always be 1.
- ///
- /// Now, let's say we have an array:
- /// [ 1, 2 ]
- /// Make a navigation object:
- /// var nav = JsonDocument.Parse(...).ToNavigation();
- ///
- /// We can access the first element the same way:
- /// var number = nav[0].TryGetInt32OrDefault();
- /// The value should be 1. - ///
- public JsonNavigationElement this[int index] - { - get - { - if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); - - if (JsonElement.ValueKind == JsonValueKind.Array) - { - var len = JsonElement.GetArrayLength(); - return index < len - ? new JsonNavigationElement(JsonElement[index], index, IsStablePropertyOrder, - HasCachedProperties) - : default; - } - - if (JsonElement.ValueKind == JsonValueKind.Object) - { - if (HasCachedProperties) - { - if (_properties.Value.Length == 0) return default; - if (index >= 0 && index < _properties.Value.Length) - { - var property = _properties.Value[index]; - return new JsonNavigationElement(property.Value, property.Name, IsStablePropertyOrder, - HasCachedProperties); - } - } - - var c = 0; - IEnumerable enumerator = IsStablePropertyOrder - ? JsonElement.EnumerateObject().OrderBy(x => x.Name) - : (IEnumerable)JsonElement.EnumerateObject(); - - foreach (var property in enumerator) - { - if (c == index) - return new JsonNavigationElement(property.Value, property.Name, c, IsStablePropertyOrder, - HasCachedProperties); - c++; - } - } - - return default; - } - } - - /// - public IEnumerator GetEnumerator() - { - if (JsonElement.ValueKind == JsonValueKind.Array) - { - return new ArrayEnumeratorWrapper(JsonElement); - } - - if (JsonElement.ValueKind == JsonValueKind.Object) - { - return HasCachedProperties - ? new ObjectEnumeratorWrapper(this, _properties.Value) - : new ObjectEnumeratorWrapper(this, IsStablePropertyOrder); - } - - return Enumerable.Empty().GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/JsonNavigationElement.cs b/src/JsonEasyNavigation/JsonNavigationElement.cs deleted file mode 100644 index 384c07a..0000000 --- a/src/JsonEasyNavigation/JsonNavigationElement.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System; -using System.Buffers; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; - -namespace JsonEasyNavigation -{ - /// - /// Represents a wrapper around , which allows to navigate it's - /// properties or array items like in a dictionary or a list. - /// is read-only and immutable. - /// - public readonly partial struct JsonNavigationElement : IEquatable - { - internal bool IsStablePropertyOrder { get; } - - internal bool HasCachedProperties => _properties != default; - - /// - /// Returns true if the is null. The element can exist (i.e. - /// is true) but still be null. - /// - public bool IsNullValue => JsonElement.ValueKind == JsonValueKind.Null || !Exist; - - /// - /// Does exists in JSON structure? This allows to check if JSON element being - /// searched exist or not. - /// - /// - /// Say, there is JSON document:
- /// {
- /// "item1": 1,
- /// "item2": 2
- /// }
- ///
- /// And a navigation object:
- /// var nav = JsonDocument.Parse(...).ToNavigation();
- ///
- /// You can check if element you want exist:
- /// nav["item3"].Exist
- ///
- /// This will return false, because there is no property with name "item3".
- /// But this code will return true:
- /// nav["item1"].Exist;
- ///
- public bool Exist { get; } - - /// - /// Returns true, if the has a name (i.e. being a property). - /// - public bool HasName { get; } - - /// - /// The name of the . - /// - public string Name { get; } - - /// - /// Returns true, if the has an index in array (i.e. being an array item). - /// - public bool HasIndex { get; } - - /// - /// Return index of the in a array OR index of a property in a object. Not all properties have index, - /// ONLY those, which were accessed via the int indexer. - /// - public int Index { get; } - - /// - /// Returns true if the can be enumerated as an array or an object. - /// - public bool IsEnumerable => JsonElement.ValueKind == JsonValueKind.Array || - JsonElement.ValueKind == JsonValueKind.Object; - - /// - /// An actual being wrapped around. - /// - public JsonElement JsonElement { get; } - - private readonly Lazy _properties; - - private JsonNavigationElement(JsonElement jsonElement, string name, int index, bool isStablePropertyOrder, bool precacheProperties) : this( - jsonElement, isStablePropertyOrder, precacheProperties) - { - Name = name; - HasName = true; - Index = index; - HasIndex = true; - } - - private JsonNavigationElement(JsonElement jsonElement, string name, bool isStablePropertyOrder, bool precacheProperties) : this( - jsonElement, isStablePropertyOrder, precacheProperties) - { - Name = name; - HasName = true; - } - - private JsonNavigationElement(JsonElement jsonElement, int index, bool isStablePropertyOrder, bool precacheProperties) : this( - jsonElement, isStablePropertyOrder, precacheProperties) - { - Index = index; - HasIndex = true; - } - - internal JsonNavigationElement(JsonProperty jsonProperty, int index, bool isStablePropertyOrder, - bool cacheProperties) : this(jsonProperty.Value, jsonProperty.Name, index, isStablePropertyOrder, - cacheProperties) - { - - } - - internal JsonNavigationElement(JsonElement jsonElement, bool isStablePropertyOrder, bool cacheProperties) - { - IsStablePropertyOrder = isStablePropertyOrder; - JsonElement = jsonElement; - Exist = !default(JsonElement).Equals(jsonElement); - Name = null; - Index = 0; - HasIndex = false; - HasName = false; - - _properties = cacheProperties - ? new Lazy(() => isStablePropertyOrder - ? jsonElement.EnumerateObject().OrderBy(x => x.Name).ToArray() - : jsonElement.EnumerateObject().ToArray(), LazyThreadSafetyMode.ExecutionAndPublication) - : default; - } - - /// - /// Returns wrapped as a or it's default value, if this element is not a string. - /// This method should never throw an exception. May allocate is the heap. - /// - public string GetStringOrDefault() - { - return JsonElement.ValueKind == JsonValueKind.String ? JsonElement.ToString() : default; - } - - /// - /// Returns wrapped as a or , if this element is not a string. - /// This method should never throw an exception. May allocate is the heap. - /// - public string GetStringOrEmpty() => GetStringOrDefault() ?? string.Empty; - - /// - /// Returns wrapped as an or it's default value. - /// - public int GetInt32OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetInt32(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public long GetInt64OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetInt64(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public short GetInt16OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetInt16(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public uint GetUInt32OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetUInt32(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public ulong GetUInt64OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetUInt64(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public ushort GetUInt16OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetUInt16(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public decimal GetDecimalOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetDecimal(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public double GetDoubleOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetDouble(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public byte GetByteOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetByte(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public sbyte GetSByteOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetSByte(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public float GetSingleOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.Number) return default; - return JsonElement.TryGetSingle(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public Guid GetGuidOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.String) return default; - return JsonElement.TryGetGuid(out var value) ? value : default; - } - - /// - /// Returns wrapped as a or it's default value. - /// - public DateTime GetDateTimeOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.String) return default; - return JsonElement.TryGetDateTime(out var value) ? value : default; - } - - /// - /// Returns wrapped as an or it's default value. - /// - public DateTimeOffset GetDateTimeOffsetOrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.String) return default; - return JsonElement.TryGetDateTimeOffset(out var value) ? value : default; - } - - /// - /// Returns wrapped as an array of or it's default value. - /// - public byte[] GetBytesFromBase64OrDefault() - { - if (JsonElement.ValueKind != JsonValueKind.String) return default; - return JsonElement.TryGetBytesFromBase64(out var value) ? value : Array.Empty(); - } - - /// - /// Returns wrapped as a or it's default value. - /// - public Stream GetStreamFromBase64OrDefault() - { - return JsonElement.ValueKind != JsonValueKind.String - ? Stream.Null - : new MemoryStream(GetBytesFromBase64OrDefault()); - } - - /// - /// Returns wrapped as a or it's default value. - /// - public bool GetBooleanOrDefault() - { - return JsonElement.ValueKind == JsonValueKind.True; - } - - /// - /// Tries to get value as of type or returns false, if this fails. - /// - public bool TryGetValue(out T value) - { - return PrimitiveValueExtractor.TryGetValue(JsonElement, out value); - } - - /// - /// Tries to get value as of type or returns it's default value, - /// if this fails. - /// - public T GetValueOrDefault() - { - return PrimitiveValueExtractor.TryGetValue(JsonElement, out T value) ? value : default; - } - - /// - /// Maps the and all it's descendants to the specified type. Mapping is provided by - /// and relies on it's implementation. May throw exceptions. - /// - public T Map(JsonSerializerOptions options = default) - { - // TODO: replace with Utf8JsonWriter - return JsonSerializer.Deserialize(JsonElement.GetRawText(), options); - } - - /// - public bool Equals(JsonNavigationElement other) - { - return JsonElement.Equals(other.JsonElement); - } - - /// - public override bool Equals(object obj) - { - return obj is JsonNavigationElement other && Equals(other); - } - - /// - public override int GetHashCode() - { - return JsonElement.GetHashCode(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/ObjectEnumeratorWrapper.cs b/src/JsonEasyNavigation/ObjectEnumeratorWrapper.cs deleted file mode 100644 index 068fc53..0000000 --- a/src/JsonEasyNavigation/ObjectEnumeratorWrapper.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - internal struct ObjectEnumeratorWrapper : IEnumerator, IEnumerator> - { - private readonly JsonNavigationElement _element; - private readonly IEnumerator _enumerator; - private int counter; - - public ObjectEnumeratorWrapper(JsonNavigationElement element, bool stable) - { - _element = element; - var jsonElement = element.JsonElement; - - if (jsonElement.ValueKind != JsonValueKind.Object) - throw new InvalidOperationException("JsonElement must be of 'Object' kind"); - - var enumerator = jsonElement.EnumerateObject(); - _enumerator = stable ? enumerator.OrderBy(x => x.Name).GetEnumerator() : enumerator; - counter = -1; - } - - public ObjectEnumeratorWrapper(JsonNavigationElement element, IEnumerable jsonProperties) - { - _enumerator = jsonProperties.GetEnumerator(); - _element = element; - counter = -1; - } - - public bool MoveNext() - { - ++counter; - return _enumerator.MoveNext(); - } - - public void Reset() - { - _enumerator.Reset(); - counter = -1; - } - - KeyValuePair IEnumerator>.Current => - new KeyValuePair(_enumerator.Current.Name, Current); - - public JsonNavigationElement Current => new JsonNavigationElement(_enumerator.Current, - counter, _element.IsStablePropertyOrder, _element.HasCachedProperties); - - object IEnumerator.Current => Current; - - public void Dispose() - { - _enumerator.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/PrimitiveValueExtractor.cs b/src/JsonEasyNavigation/PrimitiveValueExtractor.cs deleted file mode 100644 index fe655af..0000000 --- a/src/JsonEasyNavigation/PrimitiveValueExtractor.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - internal static class PrimitiveValueExtractor - { - public static bool TryGetValue(JsonElement element, out T value) - { - var type = typeof(T); - value = default; - var isSuccess = false; - - if (type == typeof(int) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetInt32(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(short) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetInt16(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(long) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetInt64(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(uint) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetUInt32(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(ushort) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetUInt16(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(ulong) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetUInt64(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(byte) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetByte(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(sbyte) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetSByte(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(decimal) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetDecimal(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(float) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetSingle(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(double) && element.ValueKind == JsonValueKind.Number) - { - isSuccess = element.TryGetDouble(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(string) && element.ValueKind == JsonValueKind.String) - { - isSuccess = true; - value = BoxingSafeConverter.Instance.Convert(element.ToString()); - } - else if (type == typeof(bool)) - { - if (element.ValueKind == JsonValueKind.True || element.ValueKind == JsonValueKind.False) - { - isSuccess = true; - value = BoxingSafeConverter.Instance.Convert(element.ValueKind == JsonValueKind.True); - } - } - else if (type == typeof(DateTime) && element.ValueKind == JsonValueKind.String) - { - isSuccess = element.TryGetDateTime(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(DateTimeOffset) && element.ValueKind == JsonValueKind.String) - { - isSuccess = element.TryGetDateTimeOffset(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(Guid) && element.ValueKind == JsonValueKind.String) - { - isSuccess = element.TryGetGuid(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(byte[]) && element.ValueKind == JsonValueKind.String) - { - isSuccess = element.TryGetBytesFromBase64(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); - } - else if (type == typeof(Stream) && element.ValueKind == JsonValueKind.String) - { - isSuccess = element.TryGetBytesFromBase64(out var v); - if (isSuccess) value = BoxingSafeConverter.Instance.Convert(new MemoryStream(v)); - } - - return isSuccess; - } - } -} \ No newline at end of file diff --git a/src/JsonEasyNavigation/PropertyPathVisitor.cs b/src/JsonEasyNavigation/PropertyPathVisitor.cs deleted file mode 100644 index a991a63..0000000 --- a/src/JsonEasyNavigation/PropertyPathVisitor.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; - -namespace JsonEasyNavigation -{ - internal static class PropertyPathVisitor - { - public static JsonNavigationElement Visit(JsonElement jsonElement, ArraySegment properties) - { - while (true) - { - if (properties.Count < 1) - { - return new JsonNavigationElement(jsonElement, false, false); - } - - var property = properties.First(); - if (jsonElement.ValueKind != JsonValueKind.Object) return default; - - if (!jsonElement.TryGetProperty(property, out var result)) return default; - - jsonElement = result; - - var offset = properties.Offset + 1; - if (offset > properties.Array.Length) break; - - properties = new ArraySegment(properties.Array, - offset, - properties.Count - 1); - } - - return default; - } - } -} \ No newline at end of file diff --git a/src/JsonExtensions.cs b/src/JsonExtensions.cs new file mode 100644 index 0000000..4de6e05 --- /dev/null +++ b/src/JsonExtensions.cs @@ -0,0 +1,82 @@ +namespace JsonEasyNavigation; + +/// +/// Various , and extensions +/// and utilities. +/// +public static class JsonExtensions +{ + /// + /// Makes the and all it's descendants to have a stable order of the + /// properties in object-kind elements. + /// + public static JsonNavigationElement WithStablePropertyOrder(this JsonNavigationElement jsonNavigationElement) => jsonNavigationElement.IsStablePropertyOrder + ? jsonNavigationElement + : new(jsonNavigationElement.JsonElement, true, jsonNavigationElement.HasCachedProperties); + + /// + /// Makes the and all it's descendants to have an unstable order (well, + /// may be) of the properties in object-kind elements. This does not imply that elements will definitely have + /// random order, but the order is not guaranteed. + /// + public static JsonNavigationElement WithoutStablePropertyOrder(this JsonNavigationElement jsonNavigationElement) => jsonNavigationElement.IsStablePropertyOrder + ? new(jsonNavigationElement.JsonElement, false, jsonNavigationElement.HasCachedProperties) + : jsonNavigationElement; + + /// + /// Makes the and all it's descendants to have properties cached in an array. + /// May be useful if is being used multiple time to access it's + /// properties. Allocates memory in heap (just once, when enumeration is executed). + /// + public static JsonNavigationElement WithCachedProperties(this JsonNavigationElement jsonNavigationElement) => jsonNavigationElement.HasCachedProperties + ? jsonNavigationElement + : new(jsonNavigationElement.JsonElement, + jsonNavigationElement.IsStablePropertyOrder, true); + + /// + /// Makes the and all it's descendants to have all properties not to + /// be cached. + /// + /// + /// + public static JsonNavigationElement WithoutCachedProperties(this JsonNavigationElement jsonNavigationElement) => jsonNavigationElement.HasCachedProperties + ? new(jsonNavigationElement.JsonElement, + jsonNavigationElement.IsStablePropertyOrder, false) + : jsonNavigationElement; + + /// + /// Creates a wrapper from , which allows to + /// navigate through it's properties and array items like using dictionary or list. + /// + public static JsonNavigationElement ToNavigation(this JsonElement jsonElement) => new(jsonElement, false, false); + + /// + /// Creates a wrapper from , which allows to + /// navigate through it's properties and array items like using dictionary or list. + /// + public static JsonNavigationElement ToNavigation(this JsonDocument jsonDocument) => new(jsonDocument.RootElement, false, false); + + /// + /// Allows to navigate in-depth through properties by their names. + /// + public static JsonNavigationElement Property(this JsonElement jsonElement, params string[] propertyPath) => PropertyPathVisitor.Visit(jsonElement, new(propertyPath)); + + /// + /// Allows to navigate in-depth through properties by their names. + /// + public static JsonNavigationElement Property(this JsonDocument jsonDocument, params string[] propertyPath) => jsonDocument.RootElement.Property(propertyPath); + + /// + /// Apply selector to all the one-by-one and return an enumerable with + /// resulting . + /// + public static IEnumerable NavigateSelect(this IEnumerable jsonDocuments, + Func selector) => jsonDocuments.Select(x => x.RootElement).NavigateSelect(selector); + + /// + /// Apply selector to all the one-by-one and return an enumerable with + /// resulting . + /// + public static IEnumerable NavigateSelect(this IEnumerable jsonElements, + Func selector) => jsonElements.Select(element => selector(element.ToNavigation())); +} \ No newline at end of file diff --git a/src/JsonNavigationElement.ReadOnlyDictionary.cs b/src/JsonNavigationElement.ReadOnlyDictionary.cs new file mode 100644 index 0000000..49bffc2 --- /dev/null +++ b/src/JsonNavigationElement.ReadOnlyDictionary.cs @@ -0,0 +1,96 @@ +namespace JsonEasyNavigation; + +public readonly partial struct JsonNavigationElement : IReadOnlyDictionary +{ + /// + /// Returns a property of having a given name. If there is no such property or + /// is not an object at all, and empty + /// will be returned (and it's Exist property will be false). + /// + /// + /// Say, there is JSON document:
+ /// {
+ /// "item1": 1,
+ /// "item2": 2
+ /// }
+ /// And a navigation object:
+ /// var nav = JsonDocument.Parse(...).ToNavigation();
+ ///
+ /// We can get property using the indexer of :
+ /// var number = nav["item1"].TryGetInt32OrDefault();
+ /// The number is 1, as expected. + ///
+ public JsonNavigationElement this[string property] => JsonElement.ValueKind != JsonValueKind.Object + ? default + : JsonElement.TryGetProperty(property, out var p) + ? new JsonNavigationElement(p, property, IsStablePropertyOrder, HasCachedProperties) + : default; + + /// + /// A total number of array items or property count in the or 0 for other kind of elements. + /// + public int Count => JsonElement.ValueKind switch + { + JsonValueKind.Array => JsonElement.GetArrayLength(), + JsonValueKind.Object => HasCachedProperties ? _properties.Value.Length : JsonElement.EnumerateObject().Count(), + _ => default + }; + + /// + IEnumerator> IEnumerable>.GetEnumerator() => JsonElement.ValueKind != JsonValueKind.Object + ? Enumerable.Empty>().GetEnumerator() + : HasCachedProperties + ? new(this, _properties.Value) + : new ObjectEnumeratorWrapper(this, IsStablePropertyOrder); + + /// + /// Returns true if the is an object and contains a property with a give name. + /// + public bool ContainsKey(string key) => this[key].Exist; + + /// + /// Returns true and a if the contains a property + /// of a given name. + /// + public bool TryGetValue(string key, out JsonNavigationElement value) + { + value = this[key]; + return value.Exist; + } + + /// + /// Returns all property names of the . + /// + public IEnumerable Keys => JsonElement.ValueKind != JsonValueKind.Object + ? Enumerable.Empty() + : HasCachedProperties + ? _properties.Value.Select(x => x.Name) + : JsonElement.EnumerateObject().Select(x => x.Name); + + /// + /// Returns all properties or array items of the or empty enumerable. + /// + public IEnumerable Values + { + get + { + var isStable = IsStablePropertyOrder; + var preCache = HasCachedProperties; + + switch (JsonElement.ValueKind) + { + case JsonValueKind.Array: + return JsonElement.EnumerateArray().Select(x => new JsonNavigationElement(x, isStable, preCache)); + case JsonValueKind.Object: + { + var properties = HasCachedProperties + ? _properties.Value + : (IEnumerable)JsonElement.EnumerateObject(); + return properties.Select(x => new JsonNavigationElement(x.Value, isStable, preCache)); + } + default: + return Enumerable.Empty(); + } + } + } +} \ No newline at end of file diff --git a/src/JsonNavigationElement.ReadOnlyList.cs b/src/JsonNavigationElement.ReadOnlyList.cs new file mode 100644 index 0000000..1230688 --- /dev/null +++ b/src/JsonNavigationElement.ReadOnlyList.cs @@ -0,0 +1,98 @@ +using System.Collections; + +namespace JsonEasyNavigation; + +public readonly partial struct JsonNavigationElement : IReadOnlyList +{ + /// + /// Returns a of the at required position. This + /// index works for both arrays and objects (the latter returns properties). Keep in mind, that the order of + /// properties in an object is not guaranteed. You should use + /// to persist the order of properties (sorted by their names). + /// + /// + /// Say, there is JSON document:
+ /// {
+ /// "item1": 1,
+ /// "item2": 2
+ /// }
+ /// And a navigation object:
+ /// var nav = JsonDocument.Parse(...).ToNavigation();
+ ///
+ /// We can get property using the indexer of :
+ /// var number = nav[0].TryGetInt32OrDefault();
+ /// The number should be 1.
+ ///
+ /// To be sure, that properties have predicted order, we can use method :
+ /// var number = nav.WithStablePropertyOrder()[0];
+ /// The number value is always be 1.
+ ///
+ /// Now, let's say we have an array:
+ /// [ 1, 2 ]
+ /// Make a navigation object:
+ /// var nav = JsonDocument.Parse(...).ToNavigation();
+ ///
+ /// We can access the first element the same way:
+ /// var number = nav[0].TryGetInt32OrDefault();
+ /// The value should be 1. + ///
+ public JsonNavigationElement this[int index] + { + get + { + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); + + switch (JsonElement.ValueKind) + { + case JsonValueKind.Array: + { + var len = JsonElement.GetArrayLength(); + return index < len + ? new JsonNavigationElement(JsonElement[index], index, IsStablePropertyOrder, + HasCachedProperties) + : default; + } + case JsonValueKind.Object: + { + if (HasCachedProperties) + { + if (_properties.Value.Length == 0) return default; + if (index >= 0 && index < _properties.Value.Length) + { + var property = _properties.Value[index]; + return new(property.Value, property.Name, IsStablePropertyOrder, + HasCachedProperties); + } + } + + var c = 0; + var enumerator = IsStablePropertyOrder + ? JsonElement.EnumerateObject().OrderBy(x => x.Name) + : (IEnumerable)JsonElement.EnumerateObject(); + + foreach (var property in enumerator) + { + if (c == index) + return new(property.Value, property.Name, c, IsStablePropertyOrder, HasCachedProperties); + c++; + } + + break; + } + } + + return default; + } + } + + /// + public IEnumerator GetEnumerator() => JsonElement.ValueKind switch + { + JsonValueKind.Array => new ArrayEnumeratorWrapper(JsonElement), + JsonValueKind.Object => HasCachedProperties ? new(this, _properties.Value) : new ObjectEnumeratorWrapper(this, IsStablePropertyOrder), + _ => Enumerable.Empty().GetEnumerator() + }; + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/JsonNavigationElement.cs b/src/JsonNavigationElement.cs new file mode 100644 index 0000000..9010846 --- /dev/null +++ b/src/JsonNavigationElement.cs @@ -0,0 +1,311 @@ +using System.Diagnostics; + +namespace JsonEasyNavigation; + +/// +/// Represents a wrapper around , which allows to navigate it's +/// properties or array items like in a dictionary or a list. +/// is read-only and immutable. +/// +public readonly partial struct JsonNavigationElement : IEquatable +{ + internal bool IsStablePropertyOrder { get; } + + internal bool HasCachedProperties => _properties != default; + + /// + /// Returns true if the is null. The element can exist (i.e. + /// is true) but still be null. + /// + public bool IsNullValue => JsonElement.ValueKind == JsonValueKind.Null || !Exist; + + /// + /// Does exists in JSON structure? This allows to check if JSON element being + /// searched exist or not. + /// + /// + /// Say, there is JSON document:
+ /// {
+ /// "item1": 1,
+ /// "item2": 2
+ /// }
+ ///
+ /// And a navigation object:
+ /// var nav = JsonDocument.Parse(...).ToNavigation();
+ ///
+ /// You can check if element you want exist:
+ /// nav["item3"].Exist
+ ///
+ /// This will return false, because there is no property with name "item3".
+ /// But this code will return true:
+ /// nav["item1"].Exist;
+ ///
+ public bool Exist { get; } + + /// + /// Returns true, if the has a name (i.e. being a property). + /// + public bool HasName { get; } + + /// + /// The name of the . + /// + public string Name { get; } + + /// + /// Returns true, if the has an index in array (i.e. being an array item). + /// + public bool HasIndex { get; } + + /// + /// Return index of the in a array OR index of a property in a object. Not all properties have index, + /// ONLY those, which were accessed via the int indexer. + /// + public int Index { get; } + + /// + /// Returns true if the can be enumerated as an array or an object. + /// + public bool IsEnumerable => JsonElement.ValueKind is JsonValueKind.Array or JsonValueKind.Object; + + /// + /// An actual being wrapped around. + /// + public JsonElement JsonElement { get; } + + private readonly Lazy _properties; + + private JsonNavigationElement(JsonElement jsonElement, string name, int index, bool isStablePropertyOrder, bool precacheProperties) : this(jsonElement, isStablePropertyOrder, precacheProperties) + { + Name = name; + HasName = true; + Index = index; + HasIndex = true; + } + + private JsonNavigationElement(JsonElement jsonElement, string name, bool isStablePropertyOrder, bool precacheProperties) : this(jsonElement, isStablePropertyOrder, precacheProperties) + { + Name = name; + HasName = true; + } + + private JsonNavigationElement(JsonElement jsonElement, int index, bool isStablePropertyOrder, bool precacheProperties) : this(jsonElement, isStablePropertyOrder, precacheProperties) + { + Index = index; + HasIndex = true; + } + + internal JsonNavigationElement(JsonProperty jsonProperty, int index, bool isStablePropertyOrder, bool cacheProperties) : this(jsonProperty.Value, jsonProperty.Name, index, isStablePropertyOrder, cacheProperties) + { + } + + internal JsonNavigationElement(JsonElement jsonElement, bool isStablePropertyOrder, bool cacheProperties) + { + IsStablePropertyOrder = isStablePropertyOrder; + JsonElement = jsonElement; + Exist = !default(JsonElement).Equals(jsonElement); + Name = null; + Index = 0; + HasIndex = false; + HasName = false; + + _properties = cacheProperties + ? new Lazy(() => isStablePropertyOrder + ? jsonElement.EnumerateObject().OrderBy(x => x.Name).ToArray() + : jsonElement.EnumerateObject().ToArray(), LazyThreadSafetyMode.ExecutionAndPublication) + : default; + } + + /// + /// Returns wrapped as a or it's default value, if this element is not a string. + /// This method should never throw an exception. May allocate is the heap. + /// + public string GetStringOrDefault() => JsonElement.ValueKind == JsonValueKind.String ? JsonElement.ToString() : default; + + /// + /// Returns wrapped as a or , if this element is not a string. + /// This method should never throw an exception. May allocate is the heap. + /// + public string GetStringOrEmpty() => GetStringOrDefault() ?? string.Empty; + + /// + /// Returns wrapped as an or it's default value. + /// + public int GetInt32OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetInt32(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public long GetInt64OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetInt64(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public short GetInt16OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetInt16(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public uint GetUInt32OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetUInt32(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public ulong GetUInt64OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetUInt64(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public ushort GetUInt16OrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetUInt16(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public decimal GetDecimalOrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetDecimal(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public double GetDoubleOrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetDouble(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public byte GetByteOrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetByte(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public sbyte GetSByteOrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetSByte(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public float GetSingleOrDefault() => JsonElement.ValueKind != JsonValueKind.Number + ? default + : JsonElement.TryGetSingle(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public Guid GetGuidOrDefault() => JsonElement.ValueKind != JsonValueKind.String + ? default + : JsonElement.TryGetGuid(out var value) + ? value + : default; + + /// + /// Returns wrapped as a or it's default value. + /// + public DateTime GetDateTimeOrDefault() => JsonElement.ValueKind != JsonValueKind.String + ? default + : JsonElement.TryGetDateTime(out var value) + ? value + : default; + + /// + /// Returns wrapped as an or it's default value. + /// + public DateTimeOffset GetDateTimeOffsetOrDefault() => JsonElement.ValueKind != JsonValueKind.String + ? default + : JsonElement.TryGetDateTimeOffset(out var value) + ? value + : default; + + /// + /// Returns wrapped as an array of or it's default value. + /// + public byte[] GetBytesFromBase64OrDefault() => JsonElement.ValueKind != JsonValueKind.String + ? default + : JsonElement.TryGetBytesFromBase64(out var value) + ? value + : []; + + /// + /// Returns wrapped as a or it's default value. + /// + public Stream GetStreamFromBase64OrDefault() => JsonElement.ValueKind != JsonValueKind.String + ? Stream.Null + : new MemoryStream(GetBytesFromBase64OrDefault()); + + /// + /// Returns wrapped as a or it's default value. + /// + public bool GetBooleanOrDefault() => JsonElement.ValueKind == JsonValueKind.True; + + /// + /// Tries to get value as of type or returns false, if this fails. + /// + public bool TryGetValue(out T value) => PrimitiveValueExtractor.TryGetValue(JsonElement, out value); + + /// + /// Tries to get value as of type or returns it's default value, + /// if this fails. + /// + public T GetValueOrDefault() => PrimitiveValueExtractor.TryGetValue(JsonElement, out T value) ? value : default; + + /// + /// Maps the and all it's descendants to the specified type. Mapping is provided by + /// and relies on it's implementation. May throw exceptions. + /// + public T Map(JsonSerializerOptions options = default) + { + options ??= new() + { + PropertyNameCaseInsensitive = true + }; + + return JsonSerializer.Deserialize(JsonElement.GetRawText(), options); + } + + /// + public bool Equals(JsonNavigationElement other) => JsonElement.Equals(other.JsonElement); + + /// + public override bool Equals(object obj) => obj is JsonNavigationElement other && Equals(other); + + /// + public override int GetHashCode() => JsonElement.GetHashCode(); +} \ No newline at end of file diff --git a/src/ObjectEnumeratorWrapper.cs b/src/ObjectEnumeratorWrapper.cs new file mode 100644 index 0000000..0556b93 --- /dev/null +++ b/src/ObjectEnumeratorWrapper.cs @@ -0,0 +1,51 @@ +using System.Collections; + +namespace JsonEasyNavigation; + +internal struct ObjectEnumeratorWrapper : IEnumerator, IEnumerator> +{ + private readonly JsonNavigationElement _element; + private readonly IEnumerator _enumerator; + private int counter; + + public ObjectEnumeratorWrapper(JsonNavigationElement element, bool stable) + { + _element = element; + var jsonElement = element.JsonElement; + + if (jsonElement.ValueKind != JsonValueKind.Object) + throw new InvalidOperationException("JsonElement must be of 'Object' kind"); + + var enumerator = jsonElement.EnumerateObject(); + _enumerator = stable ? enumerator.OrderBy(x => x.Name).GetEnumerator() : enumerator; + counter = -1; + } + + public ObjectEnumeratorWrapper(JsonNavigationElement element, IEnumerable jsonProperties) + { + _enumerator = jsonProperties.GetEnumerator(); + _element = element; + counter = -1; + } + + public bool MoveNext() + { + ++counter; + return _enumerator.MoveNext(); + } + + public void Reset() + { + _enumerator.Reset(); + counter = -1; + } + + KeyValuePair IEnumerator>.Current => new(_enumerator.Current.Name, Current); + + public JsonNavigationElement Current => new(_enumerator.Current, + counter, _element.IsStablePropertyOrder, _element.HasCachedProperties); + + object IEnumerator.Current => Current; + + public void Dispose() => _enumerator.Dispose(); +} \ No newline at end of file diff --git a/src/PrimitiveValueExtractor.cs b/src/PrimitiveValueExtractor.cs new file mode 100644 index 0000000..a555351 --- /dev/null +++ b/src/PrimitiveValueExtractor.cs @@ -0,0 +1,107 @@ +namespace JsonEasyNavigation; + +internal static class PrimitiveValueExtractor +{ + public static bool TryGetValue(JsonElement element, out T value) + { + var type = typeof(T); + value = default; + var isSuccess = false; + + if (type == typeof(int) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetInt32(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(short) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetInt16(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(long) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetInt64(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(uint) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetUInt32(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(ushort) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetUInt16(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(ulong) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetUInt64(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(byte) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetByte(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(sbyte) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetSByte(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(decimal) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetDecimal(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(float) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetSingle(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(double) && element.ValueKind == JsonValueKind.Number) + { + isSuccess = element.TryGetDouble(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(string) && element.ValueKind == JsonValueKind.String) + { + isSuccess = true; + value = BoxingSafeConverter.Instance.Convert(element.ToString()); + } + else if (type == typeof(bool)) + { + if (element.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + isSuccess = true; + value = BoxingSafeConverter.Instance.Convert(element.ValueKind == JsonValueKind.True); + } + } + else if (type == typeof(DateTime) && element.ValueKind == JsonValueKind.String) + { + isSuccess = element.TryGetDateTime(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(DateTimeOffset) && element.ValueKind == JsonValueKind.String) + { + isSuccess = element.TryGetDateTimeOffset(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(Guid) && element.ValueKind == JsonValueKind.String) + { + isSuccess = element.TryGetGuid(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(byte[]) && element.ValueKind == JsonValueKind.String) + { + isSuccess = element.TryGetBytesFromBase64(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(v); + } + else if (type == typeof(Stream) && element.ValueKind == JsonValueKind.String) + { + isSuccess = element.TryGetBytesFromBase64(out var v); + if (isSuccess) value = BoxingSafeConverter.Instance.Convert(new MemoryStream(v)); + } + + return isSuccess; + } +} \ No newline at end of file diff --git a/src/PropertyPathVisitor.cs b/src/PropertyPathVisitor.cs new file mode 100644 index 0000000..2846ca5 --- /dev/null +++ b/src/PropertyPathVisitor.cs @@ -0,0 +1,27 @@ +namespace JsonEasyNavigation; + +internal static class PropertyPathVisitor +{ + public static JsonNavigationElement Visit(JsonElement jsonElement, ArraySegment properties) + { + while (true) + { + if (properties.Count < 1) + return new(jsonElement, false, false); + + var property = properties.First(); + if (jsonElement.ValueKind != JsonValueKind.Object) return default; + + if (!jsonElement.TryGetProperty(property, out var result)) return default; + + jsonElement = result; + + var offset = properties.Offset + 1; + if (offset > properties.Array.Length) break; + + properties = new(properties.Array, offset, properties.Count - 1); + } + + return default; + } +} \ No newline at end of file diff --git a/src/README_BODY.md b/src/README_BODY.md new file mode 100644 index 0000000..8ed698a --- /dev/null +++ b/src/README_BODY.md @@ -0,0 +1,120 @@ +![JsonEasyNavigation](src/Resources/project.png "JsonEasyNavigation") + +
+ +## Navigation +* Introduction +* Usage +* Features +* Installation +* License + +
+ +

Introduction

+ +This library provides a wrapper class around Microsoft's .NET JsonElement JsonElement (located in System.Text.Json) which allows to navigate through JSON DOM (domain object model) hierarchy using indexer-style syntax (as in collections and dictionaries) for properties and array alike. It also contains useful methods to get values without throwing exceptions. +Target frameworks are .NET 5 and NET Standard 2.0. + +

Usage

+ +Here is an example: + +```JSON +{ + "Persons": [ + { + "Id": 0, + "Name": "John", + "SecondName": "Wick", + "NickName": "Baba Yaga" + }, + { + "Id": 1, + "Name": "Wade", + "SecondName": "Winston", + "NickName": "Deadpool" + } + ] +} +``` + +Assume that we are using `System.Text.Json` so we can create JsonDocument: + +```C# +var jsonDocument = JsonDocument.Parse(json); +``` + +Then we convert this JSON document to the `JsonNavigationElement` provided by JsonEasyNavigation library: + +```C# +var nav = jsonDocument.ToNavigation(); +``` + +`JsonNavigationElement` is a struct, a wrapper around JsonElement. This struct provides many useful methods to operate arrays, objects and getting values from the JsonElement inside. + +Now we can easley navigate Domain Object Model using indexers in a sequential style: + +```C# +var arrayItem = nav[0]; // first item in the array +var id = arrayItem["Id"].GetInt32OrDefault(); // 0 +var nickName = arrayItem["NickName"].GetStringOrDefault(); // "Baba Yaga" +``` + +Notice the usage of `GetXxxOrDefault` methods, which provides a convenient way to get values from the JsonElement without throwing exceptions. There are a lot of other similar useful methods. + +We also can check if the property exist: + +```C# +if (nav[0]["Age"].Exist) +{ + // Do something if the Age property of the first object in array exist. +} +``` + +`JsonNavigationElement` does **not** throw exception if a property or array item does not exist. You can always check `Exist` property of an `JsonNavigationElement` to be sure that corresponding `JsonElement` was found. + +It is also possible to map `JsonNavigationElement` into the object of the specific type (using JsonSerializer internally): + +```C# +public class Person +{ + public int Id { get; set; } + public string Name { get; set; } + public string SecondName { get; set; } + public string NickName { get; set; } +} + +// ... + +var person = nav[0].Map(); +``` + +

Features

+ +Overall, the library provides following features: + +* A wrapper around JsonElement encapsulating all behaviour regarding to the DOM traversal and navigation (`JsonNavigationElement`); +* The API is implemented in a no-throw manner - you can "get" properties that don't exist in the DOM and check their existence; +* Implementation of a `IReadOnlyDictionary` and `IReadOnlyCollection`; +* Methods for converting values to the specified types in a type-safe way (and also generic methods like `TryGetValue`); +* Extensions for caching properties and persisting their order for faster and easier JSON navigation. + +

Installation

+ +This library can be installed using NuGet found [here](https://www.nuget.org/packages/JsonEasyNavigation/). +This library can be installed using GitHub found [here](https://github.com/users/Latency/packages/nuget/package/JsonEasyNavigation). + +

License

+ +The source code for the site is licensed under the MIT license, which you can find in +the [MIT-LICENSE].txt file. + +All graphical assets are licensed under the +[Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). + +[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job.) + + [GNU LESSER GENERAL PUBLIC LICENSE]: + [MSDN article]: + [MIT-License]: \ No newline at end of file