Skip to content

Commit

Permalink
Use JsonPath.Net for JSONPath selectors (Lombiq Technologies: OCORE-1…
Browse files Browse the repository at this point in the history
…48) (#15524)
  • Loading branch information
sarahelsaig authored Mar 21, 2024
1 parent 5d67de4 commit bf0afdb
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 136 deletions.
1 change: 1 addition & 0 deletions src/OrchardCore.Build/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageManagement Include="GraphQL.MicrosoftDI" Version="7.8.0" />
<PackageManagement Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageManagement Include="Jint" Version="3.0.1" />
<PackageManagement Include="JsonPath.Net" Version="1.0.0" />
<PackageManagement Include="HtmlSanitizer" Version="8.1.860-beta" />
<PackageManagement Include="Irony.Core" Version="1.0.7" />
<PackageManagement Include="libphonenumber-csharp" Version="8.13.32" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,18 +252,10 @@ public async Task<IActionResult> Delete(string menuContentItemId, string menuIte
return Forbid();
}

ContentItem menu;

var contentTypeDefinition = await _contentDefinitionManager.GetTypeDefinitionAsync("Menu");

if (!contentTypeDefinition.IsDraftable())
{
menu = await _contentManager.GetAsync(menuContentItemId, VersionOptions.Latest);
}
else
{
menu = await _contentManager.GetAsync(menuContentItemId, VersionOptions.DraftRequired);
}
var menu = contentTypeDefinition.IsDraftable()
? await _contentManager.GetAsync(menuContentItemId, VersionOptions.DraftRequired)
: await _contentManager.GetAsync(menuContentItemId, VersionOptions.Latest);

if (menu == null)
{
Expand Down
51 changes: 0 additions & 51 deletions src/OrchardCore/OrchardCore.Abstractions/Json/Nodes/JArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,57 +106,6 @@ public static bool ContainsValue(this JsonArray? jsonArray, JsonValue? value)
return false;
}

/// <summary>
/// Selects a <see cref="JsonNode"/> from this <see cref="JsonArray"/> using a JSON path.
/// </summary>
public static JsonNode? SelectNode(this JsonArray? jsonArray, string? path)
{
path = path.GetNormalizedPath();
if (jsonArray is null || path is null)
{
return null;
}

foreach (var item in jsonArray)
{
if (item is null)
{
continue;
}

var itemPath = item.GetNormalizedPath();
if (itemPath == path)
{
return item;
}

if (itemPath is null || !path.Contains(itemPath))
{
continue;
}

if (item is JsonObject jObject)
{
var node = jObject.SelectNode(path);
if (node is not null)
{
return node;
}
}

if (item is JsonArray jArray)
{
var node = jArray.SelectNode(path);
if (node is not null)
{
return node;
}
}
}

return null;
}

/// <summary>
/// Merge the specified content into this <see cref="JsonArray"/> using <see cref="JsonMergeSettings"/>.
/// </summary>
Expand Down
36 changes: 25 additions & 11 deletions src/OrchardCore/OrchardCore.Abstractions/Json/Nodes/JNode.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Json.Path;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -170,26 +171,39 @@ jsonNode is JsonObject jsonObject && jsonObject.Count > 0 ||
public static string? GetNormalizedPath(this JsonNode jsonNode) => jsonNode.GetPath().GetNormalizedPath();

/// <summary>
/// Selects a <see cref="JsonNode"/> from this <see cref="JsonObject"/> using a JSON path.
/// Selects a <see cref="JsonNode"/> from this <see cref="JsonObject"/> using JSONPath.
/// </summary>
public static JsonNode? SelectNode(this JsonNode? jsonNode, string? path)
/// <param name="jsonNode">The JSON node which serves as the root of the current search..</param>
/// <param name="path">The JSONPath query where <c>$</c> is <paramref name="jsonNode"/>.</param>
/// <param name="options">Optional settings to configure the JSONPath parser.</param>
/// <remarks>
/// This method uses JsonPath.Net to evaluate the <paramref name="path"/>. For more information on JSONPath, see the
/// specification at https://www.rfc-editor.org/rfc/rfc9535.html or the JsonPath.Net documentation at
/// https://docs.json-everything.net/path/basics/.
/// </remarks>
public static JsonNode? SelectNode(this JsonNode jsonNode, string path, PathParsingOptions? options = null)
{
if (jsonNode is null || path is null)
{
return null;
}
ArgumentNullException.ThrowIfNull(jsonNode);
ArgumentNullException.ThrowIfNull(path);

if (jsonNode is JsonObject jsonObject)
path = path.Trim();
if (string.IsNullOrEmpty(path))
{
return jsonObject.SelectNode(path);
return jsonNode;
}

if (jsonNode is JsonArray jsonArray)
// Without this JSONPath parsing fails (JsonPath.Parse throws "PathParseException: Path must start with '$'").
// Since the path always has to start with "$.", this alteration won't cause any problems. It's also necessary
// to maintain backwards compatibility with JSONPaths written for Newtonsoft and for simple direct child
// property access too (e.g. when the path is "property" instead of "$.property").
if (path[0] != '$')
{
return jsonArray.SelectNode(path);
path = "$." + path;
}

return null;
return JsonPath.TryParse(path, out var jsonPath, options)
? jsonPath.Evaluate(jsonNode).Matches?.FirstOrDefault()?.Value
: null;
}

/// <summary>
Expand Down
51 changes: 0 additions & 51 deletions src/OrchardCore/OrchardCore.Abstractions/Json/Nodes/JObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,57 +85,6 @@ public static bool TryParse(string json, out JsonObject? jsonObject, JsonNodeOpt
/// </summary>
public static JsonObject? Clone(this JsonObject? jsonObject) => jsonObject?.DeepClone().AsObject();

/// <summary>
/// Selects a <see cref="JsonNode"/> from this <see cref="JsonObject"/> using a JSON path.
/// </summary>
public static JsonNode? SelectNode(this JsonObject? jsonObject, string? path)
{
path = path.GetNormalizedPath();
if (jsonObject is null || path is null)
{
return null;
}

foreach (var item in jsonObject)
{
if (item.Value is null)
{
continue;
}

var itemPath = item.Value.GetNormalizedPath();
if (itemPath == path)
{
return item.Value;
}

if (itemPath is null || !path.Contains(itemPath))
{
continue;
}

if (item.Value is JsonObject jObject)
{
var node = jObject.SelectNode(path);
if (node is not null)
{
return node;
}
}

if (item.Value is JsonArray jArray)
{
var node = jArray.SelectNode(path);
if (node is not null)
{
return node;
}
}
}

return null;
}

/// <summary>
/// Merge the specified content into this <see cref="JsonObject"/> using <see cref="JsonMergeSettings"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="JsonPath.Net" />
<PackageReference Include="ZString" />
</ItemGroup>

Expand Down
55 changes: 43 additions & 12 deletions test/OrchardCore.Tests/Data/ContentItemTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using OrchardCore.ContentManagement;
using System.Text.Json.Nodes;

namespace OrchardCore.Tests.Data
{
Expand Down Expand Up @@ -46,9 +47,7 @@ public void ShouldSerializeParts()
[Fact]
public void ShouldUpdateContent()
{
var contentItem = new ContentItem();
contentItem.GetOrCreate<MyPart>();
contentItem.Alter<MyPart>(x => x.Text = "test");
var contentItem = CreateContentItemWithMyPart();

var json = JConvert.SerializeObject(contentItem);

Expand All @@ -61,9 +60,7 @@ public void ShouldUpdateContent()
[Fact]
public void ShouldAlterPart()
{
var contentItem = new ContentItem();
contentItem.GetOrCreate<MyPart>();
contentItem.Alter<MyPart>(x => x.Text = "test");
var contentItem = CreateContentItemWithMyPart();

var json = JConvert.SerializeObject(contentItem);

Expand All @@ -76,9 +73,7 @@ public void ShouldAlterPart()
[Fact]
public void ContentShouldOnlyContainParts()
{
var contentItem = new ContentItem();
contentItem.GetOrCreate<MyPart>();
contentItem.Alter<MyPart>(x => x.Text = "test");
var contentItem = CreateContentItemWithMyPart();

var json = JConvert.SerializeObject(contentItem);

Expand All @@ -88,9 +83,7 @@ public void ContentShouldOnlyContainParts()
[Fact]
public void ContentShouldStoreFields()
{
var contentItem = new ContentItem();
contentItem.GetOrCreate<MyPart>();
contentItem.Alter<MyPart>(x => x.Text = "test");
var contentItem = CreateContentItemWithMyPart();
contentItem.Alter<MyPart>(x =>
{
x.GetOrCreate<MyField>("myField");
Expand All @@ -102,6 +95,44 @@ public void ContentShouldStoreFields()
Assert.Contains(@"""MyPart"":{""Text"":""test"",""myField"":{""Value"":123}}", json);
}

[Fact]
public void ContentShouldBeJsonPathQueryable()
{
var contentItem = CreateContentItemWithMyPart();
JsonNode contentItemJson = contentItem.Content;
JsonNode contentPartJson = contentItem.As<MyPart>().Content;

// The content part should be selectable from the content item.
var selectedItemNode = contentItemJson.SelectNode("MyPart");
Assert.NotNull(selectedItemNode);
Assert.Equal(selectedItemNode.ToJsonString(), contentPartJson.ToJsonString());

// Verify that SelectNode queries the subtree of the node it's called on (not the document root).
var textPropertyNode = selectedItemNode.SelectNode("Text");
AssertJsonEqual(textPropertyNode, JObject.Parse(selectedItemNode.ToJsonString()).SelectNode("Text"));

// Verify consistent results when targeting the same node in different ways.
AssertJsonEqual(textPropertyNode, contentPartJson.SelectNode("Text"));
AssertJsonEqual(textPropertyNode, contentItemJson.SelectNode("MyPart.Text"));
AssertJsonEqual(textPropertyNode, contentItemJson.SelectNode("$..Text"));
}

private static ContentItem CreateContentItemWithMyPart(string text = "test")
{
var contentItem = new ContentItem();
contentItem.GetOrCreate<MyPart>();
contentItem.Alter<MyPart>(x => x.Text = text);

return contentItem;
}

private static void AssertJsonEqual(JsonNode expected, JsonNode actual)
{
Assert.NotNull(expected);
Assert.NotNull(actual);
Assert.Equal(expected.ToJsonString(), actual.ToJsonString());
}

[Fact]
public void ShouldDeserializeListContentPart()
{
Expand Down

0 comments on commit bf0afdb

Please sign in to comment.