Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function #transform: handles multiple transforms #267

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion JUST.net/JUST.net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Title>JUST - JSON Under Simple Transformation</Title>
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>This a cool .NET Standard library which enables you to transform a JSON document into another JSON document using JSON transformations using JSON path. This is the JSON equivalent of XSLT.
This will repace the JUST and JUST.NETCore packages.</Description>
<Summary>This is the JSON equivalent of XSLT</Summary>
Expand Down
60 changes: 58 additions & 2 deletions JUST.net/JsonTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,63 @@ private void ParsePropertyFunction(IDictionary<string, JArray> parentArray, IDic
case "eval":
EvalOperation(property, arguments, parentArray, currentArrayToken, ref loopProperties, ref tokensToAdd);
break;
case "transform":
TranformOperation(property, arguments, parentArray, currentArrayToken);
break;
}
}

private void TranformOperation(JProperty property, string arguments, IDictionary<string, JArray> parentArray, IDictionary<string, JToken> currentArrayToken)
{
string[] argumentArr = ExpressionHelper.SplitArguments(arguments, Context.EscapeChar);

object functionResult = ParseArgument(parentArray, currentArrayToken, argumentArr[0]);
if (!(functionResult is string jsonPath))
{
throw new ArgumentException($"Invalid path for #transform: '{argumentArr[0]}' resolved to null!");
}

JToken selectedToken = null;
string alias = null;
if (argumentArr.Length > 1)
{
alias = ParseArgument(parentArray, currentArrayToken, argumentArr[1]) as string;
if (!(currentArrayToken?.ContainsKey(alias) ?? false))
{
throw new ArgumentException($"Unknown loop alias: '{argumentArr[1]}'");
}
JToken input = alias != null ? currentArrayToken?[alias] : currentArrayToken?.Last().Value ?? Context.Input;
var selectable = GetSelectableToken(currentArrayToken[alias], Context);
selectedToken = selectable.Select(argumentArr[0]);
}
else
{
var selectable = GetSelectableToken(currentArrayToken?.Last().Value ?? Context.Input, Context);
selectedToken = selectable.Select(argumentArr[0]);
}

if (property.Value.Type == JTokenType.Array)
{
JToken originalInput = Context.Input;
Context.Input = selectedToken;
for (int i = 0; i < property.Value.Count(); i++)
{
JToken token = property.Value[i];
if (token.Type == JTokenType.String)
{
var obj = ParseFunction(token.Value<string>(), parentArray, currentArrayToken);
token.Replace(GetToken(obj));
}
else
{
RecursiveEvaluate(ref token, i == 0 ? parentArray : null, i == 0 ? currentArrayToken : null);
}
Context.Input = token;
}

Context.Input = originalInput;
}
property.Parent.Replace(property.Value[property.Value.Count() - 1]);
}

private void PostOperationsBuildUp(ref JToken parentToken, List<JToken> tokenToForm)
Expand Down Expand Up @@ -902,12 +958,12 @@ private object ParseApplyOver(IDictionary<string, JArray> array, IDictionary<str
var contextInput = Context.Input;
var input = JToken.Parse(Transform(parameters[0].ToString(), contextInput.ToString()));
Context.Input = input;
if (parameters[1].ToString().Trim().Trim('\'').StartsWith('{'))
if (parameters[1].ToString().Trim().Trim('\'').StartsWith("{"))
{
var jobj = JObject.Parse(parameters[1].ToString().Trim().Trim('\''));
output = new JsonTransformer(Context).Transform(jobj, input);
}
else if (parameters[1].ToString().Trim().Trim('\'').StartsWith('['))
else if (parameters[1].ToString().Trim().Trim('\'').StartsWith("["))
{
var jarr = JArray.Parse(parameters[1].ToString().Trim().Trim('\''));
output = new JsonTransformer(Context).Transform(jarr, input);
Expand Down
89 changes: 86 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1592,9 +1592,8 @@ Output:

## <a name="applyover"></a> Apply function over transformation

Sometimes you cannnot achieve what you want directly from a single function (or composition). To overcome this you may want to apply a function over a previous transformation. That's what #applyover does.
First argument is the first transformation to apply to input, and the result will serve as input to the second argument/transformation. Second argument can be a simple function or a complex transformation (an object or an array).
Note that if any of the arguments/transformations of #applyover has commas (,), one has to use #constant_comma to represent them (and use #xconcat to construct the argument/transformation).
Sometimes you cannnot achieve what you want directly from a single function (or composition). To overcome this you may want to apply a function over a previous transformation. That's what #applyover does. First argument is the first transformation to apply to input, and the result will serve as input to the second argument/transformation. Second argument can be a simple function or a complex transformation (an object or an array).
Bare in mind that every special character (comma, parenthesis) must be escaped if they appear inside the second argument/transformation.

Consider the following input:

Expand Down Expand Up @@ -1635,6 +1634,90 @@ Output:
```


## <a name="transform"></a> Multiple transformations

The #applyover function is handy to make a simple transformation, but when extra transformation is complex, it can became cumbersome, because one has to escape all special characters.
To avoid this, there's a function called #transform. It takes a path as parameter, and like bulk functions, is composed by an array. Each element of the array is a transformation,
that will be applied over the generated result of the previous item of the array. The first item/transformation will be applied over the given input, or current element if one is on an array loop.
Note that for the second element/transformation and beyond, the input is the previous generated output of the previous transformation, so it's like a new transformation.

Consider the following input:

```JSON
{
"spell": ["one", "two", "three"],
"letters": ["z", "c", "n"],
"nested": {
"spell": ["one", "two", "three"],
"letters": ["z", "c", "n"]
},
"array": [{
"spell": ["one", "two", "three"],
"letters": ["z", "c", "n"]
}, {
"spell": ["four", "five", "six"],
"letters": ["z", "c", "n"]
}
]
}
```


Transformer:

```JSON
{
"scalar": {
"#transform($)": [
{ "condition": { "#loop($.letters)": { "test": "#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)" } } },
"#exists($.condition[?(@.test=='yes')])"
]
},
"object": {
"#transform($)": [
{ "condition": { "#loop($.letters)": { "test": "#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)" } } },
{ "intermediate_transform": "#valueof($.condition)" },
{ "result": "#exists($.intermediate_transform[?(@.test=='yes')])" }
]
},
"select_token": {
"#transform($.nested)": [
{ "condition": { "#loop($.letters)": { "test": "#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)" } } },
{ "intermediate_transform": "#valueof($.condition)" },
{ "result": "#exists($.intermediate_transform[?(@.test=='yes')])" }
]
},
"loop": {
"#loop($.array,selectLoop)": {
"#transform($)": [
{ "condition": { "#loop($.letters)": { "test": "#ifcondition(#stringcontains(#currentvalueatpath($.spell[0],selectLoop),#currentvalue()),True,yes,no)" } } },
{ "intermediate_transform": "#valueof($.condition)" },
{ "result": "#exists($.intermediate_transform[?(@.test=='yes')])" }
]
}
}
}
```

Output:

```JSON
{
"scalar": true,
"object": {
"result": true
}
"select_token": {
"result": true
},
"loop": [
{ "result": true },
{ "result": false }
]
}
```


## <a name="schemavalidation"></a> Schema Validation against multiple schemas using prefixes

A new feature to validate a JSON against multiple schemas has been introduced in the new Nuget 2.0.xxx. This is to enable namespace based validation using prefixes like in XSD.
Expand Down
2 changes: 0 additions & 2 deletions UnitTestForExternalAssemblyBug/ExternalAssemblyBugTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel.Resolution;
using NUnit.Framework;

namespace JUST.UnitTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions UnitTests/JUST.net.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
</ItemGroup>

<ItemGroup>
Expand Down
74 changes: 74 additions & 0 deletions UnitTests/MultipleTransformations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using NUnit.Framework;

namespace JUST.UnitTests
{
[TestFixture]
public class MultipleTransformations
{
[Test]
public void MultipleTransformsScalarResult()
{
const string input = "{\"d\": [ \"one\", \"two\", \"three\" ], \"values\": [ \"z\", \"c\", \"n\" ]}";
const string transformer =
"{ \"result\": " +
"{ \"#transform($)\": [ " +
"{ \"condition\": { \"#loop($.values)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.d[0]),#currentvalue()),True,yes,no)\" } } }, " +
"{ \"intermediate_transform\": \"#valueof($.condition)\" }," +
"\"#exists($.intermediate_transform[?(@.test=='yes')])\" ] } }";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider using raw string literals (e.g. "triple quotes") to make these JSONs more readable.
See: https://devblogs.microsoft.com/dotnet/csharp-11-preview-updates/

(Of course, you'll need to bump the language version to 11.)


var result = new JsonTransformer().Transform(transformer, input);

Assert.AreEqual("{\"result\":true}", result);
}

[Test]
public void MultipleTransformsObjectResult()
{
const string input = "{\"d\": [ \"one\", \"two\", \"three\" ], \"values\": [ \"z\", \"c\", \"n\" ]}";
const string transformer =
"{ \"object\": " +
"{ \"#transform($)\": [ " +
"{ \"condition\": { \"#loop($.values)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.d[0]),#currentvalue()),True,yes,no)\" } } }, " +
"{ \"intermediate_transform\": \"#valueof($.condition)\" }," +
"{ \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] } }";

var result = new JsonTransformer().Transform(transformer, input);

Assert.AreEqual("{\"object\":{\"result\":true}}", result);
}

[Test]
public void MultipleTransformsOverSelectedToken()
{
const string input = "{ \"select\": {\"d\": [ \"one\", \"two\", \"three\" ], \"values\": [ \"z\", \"c\", \"n\" ]} }";
const string transformer =
"{ \"select_token\": " +
"{ \"#transform($.select)\": [ " +
"{ \"condition\": { \"#loop($.values)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.d[0]),#currentvalue()),True,yes,no)\" } } }, " +
"{ \"intermediate_transform\": \"#valueof($.condition)\" }," +
"{ \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] } }";

var result = new JsonTransformer().Transform(transformer, input);

Assert.AreEqual("{\"select_token\":{\"result\":true}}", result);
}

[Test]
public void MultipleTransformsWithinLoop()
{
const string input = "{ \"select\": [{ \"d\": [ \"one\", \"two\", \"three\" ], \"values\": [ \"z\", \"c\", \"n\" ] }, { \"d\": [ \"four\", \"five\", \"six\" ], \"values\": [ \"z\", \"c\", \"n\" ] }] }";
const string transformer =
"{ \"loop\": {" +
" \"#loop($.select,selectLoop)\": { " +
"\"#transform($)\": [ " +
"{ \"condition\": { \"#loop($.values)\": { \"test\": \"#ifcondition(#stringcontains(#currentvalueatpath($.d[0],selectLoop),#currentvalue()),True,yes,no)\" } } }, " +
"{ \"intermediate_transform\": \"#valueof($.condition)\" }," +
"{ \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] " +
" } } }";

var result = new JsonTransformer().Transform(transformer, input);

Assert.AreEqual("{\"loop\":[{\"result\":true},{\"result\":false}]}", result);
}
}
}
11 changes: 11 additions & 0 deletions UnitTests/ReadmeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,5 +256,16 @@ public void TypeCheck()

Assert.AreEqual("{\"isNumberTrue1\":true,\"isNumberTrue2\":true,\"isNumberFalse\":false,\"isBooleanTrue\":true,\"isBooleanFalse\":false,\"isStringTrue\":true,\"isStringFalse\":false,\"isArrayTrue\":true,\"isArrayFalse\":false}", result);
}

[Test]
public void Transform()
{
const string input = "{ \"spell\": [\"one\", \"two\", \"three\"], \"letters\": [\"z\", \"c\", \"n\"], \"nested\": { \"spell\": [\"one\", \"two\", \"three\"], \"letters\": [\"z\", \"c\", \"n\"] },\"array\": [{ \"spell\": [\"one\", \"two\", \"three\"], \"letters\": [\"z\", \"c\", \"n\"] }, { \"spell\": [\"four\", \"five\", \"six\"], \"letters\": [\"z\", \"c\", \"n\"] } ]}";
const string transformer = "{ \"scalar\": { \"#transform($)\": [{ \"condition\": { \"#loop($.letters)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)\" } } }, \"#exists($.condition[?(@.test=='yes')])\"] }, \"object\": { \"#transform($)\": [{ \"condition\": { \"#loop($.letters)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)\" } } }, { \"intermediate_transform\": \"#valueof($.condition)\" }, { \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] }, \"select_token\": { \"#transform($.nested)\": [{ \"condition\": { \"#loop($.letters)\": { \"test\": \"#ifcondition(#stringcontains(#valueof($.spell[0]),#currentvalue()),True,yes,no)\" } } }, { \"intermediate_transform\": \"#valueof($.condition)\" }, { \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] }, \"loop\": { \"#loop($.array,selectLoop)\": { \"#transform($)\": [{ \"condition\": { \"#loop($.letters)\": { \"test\": \"#ifcondition(#stringcontains(#currentvalueatpath($.spell[0],selectLoop),#currentvalue()),True,yes,no)\" } } }, { \"intermediate_transform\": \"#valueof($.condition)\" }, { \"result\": \"#exists($.intermediate_transform[?(@.test=='yes')])\" } ] } } } ";

var result = new JsonTransformer().Transform(transformer, input);

Assert.AreEqual("{\"scalar\":true,\"object\":{\"result\":true},\"select_token\":{\"result\":true},\"loop\":[{\"result\":true},{\"result\":false}]}", result);
}
}
}