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 #289

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions JUST.net/ComparisonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal static class ComparisonHelper
{
public static bool Equals(object x, object y, EvaluationMode evaluationMode)
{
var comparisonType = (evaluationMode == EvaluationMode.Strict)
var comparisonType = (evaluationMode == EvaluationMode.Strict && x.GetType() != typeof(bool) && y.GetType() != typeof(bool))
? StringComparison.CurrentCulture
: StringComparison.InvariantCultureIgnoreCase;

Expand All @@ -15,11 +15,11 @@ public static bool Equals(object x, object y, EvaluationMode evaluationMode)

public static bool Contains(object x, object y, EvaluationMode evaluationMode)
{
var comparisonType = (evaluationMode == EvaluationMode.Strict)
var comparisonType = (evaluationMode == EvaluationMode.Strict && x.GetType() != typeof(bool) && y.GetType() != typeof(bool))
? StringComparison.CurrentCulture
: StringComparison.InvariantCultureIgnoreCase;

return ((x != null) && x.ToString().IndexOf(y?.ToString() ?? string.Empty, comparisonType) >= 0);
return x != null && x.ToString().IndexOf(y?.ToString() ?? string.Empty, comparisonType) >= 0;
}
}
}
114 changes: 94 additions & 20 deletions JUST.net/JsonTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ private JToken TransformValue(JToken transformer, JToken input)
{
var tmp = new JObject
{
{ "root", transformer }
{ State.RootKey, transformer }
};
Transform(tmp, input);
return tmp["root"];
return tmp[State.RootKey];
}

public JToken Transform(JObject transformer, string input)
Expand Down Expand Up @@ -148,15 +148,15 @@ private void RecursiveEvaluate(ref JToken parentToken, State state)

if (selectedTokens != null)
{
CopyPostOperationBuildUp(parentToken, selectedTokens, this.Context);
CopyPostOperationBuildUp(parentToken, selectedTokens);
}
if (tokensToReplace != null)
{
ReplacePostOperationBuildUp(parentToken, tokensToReplace, this.Context);
ReplacePostOperationBuildUp(parentToken, tokensToReplace);
}
if (tokensToDelete != null)
{
DeletePostOperationBuildUp(parentToken, tokensToDelete, this.Context);
DeletePostOperationBuildUp(parentToken, tokensToDelete);
}
if (tokensToAdd != null)
{
Expand Down Expand Up @@ -250,9 +250,77 @@ private void ParsePropertyFunction(State state, ref List<string> loopProperties,
ScopeOperation(property.Name, arguments, state, ref scopeProperties, ref scopeToForm, childToken);
isScope = true;
break;
case "transform":
TranformOperation(property, arguments, state);
break;
}
}

private void TranformOperation(JProperty property, string arguments, State state)
{
string[] argumentArr = ExpressionHelper.SplitArguments(arguments, Context.EscapeChar);

object functionResult = ParseArgument(state, 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(state, argumentArr[1]) as string;
if (!(state.CurrentArrayToken?.Any(t => t.Key.Key == alias) ?? false))
{
throw new ArgumentException($"Unknown loop alias: '{argumentArr[1]}'");
}
JToken input = alias != null ? state.CurrentArrayToken?.Single(t => t.Key.Key == alias).Value : state.CurrentArrayToken?.Last().Value ?? Context.Input;
var selectable = GetSelectableToken(state.CurrentArrayToken.Single(t => t.Key.Key == alias).Value, Context);
selectedToken = selectable.Select(argumentArr[0]);
}
else
{
var selectable = GetSelectableToken(state.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>(),
new State(token, Context.Input, _levelCounter,
state.CurrentArrayToken.Where(t => t.Key.Key != State.RootKey)
.ToDictionary(p => p.Key, p => p.Value),
state.CurrentScopeToken.Where(t => t.Key.Key != State.RootKey)
.ToDictionary(p => p.Key, p => p.Value), true));
token.Replace(GetToken(obj));
}
else
{
RecursiveEvaluate(
ref token,
new State(token, Context.Input, _levelCounter,
state.CurrentArrayToken.Where(t => t.Key.Key != State.RootKey)
.ToDictionary(p => p.Key, p => p.Value),
state.CurrentScopeToken.Where(t => t.Key.Key != State.RootKey)
.ToDictionary(p => p.Key, p => p.Value), true));
}
Context.Input = token;
}

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

private void PostOperationsBuildUp(ref JToken parentToken, List<JToken> tokenToForm)
{
if (tokenToForm != null)
Expand Down Expand Up @@ -285,15 +353,15 @@ private void PostOperationsBuildUp(ref JToken parentToken, List<JToken> tokenToF
}
}

private static void CopyPostOperationBuildUp(JToken parentToken, List<JToken> selectedTokens, JUSTContext context)
private void CopyPostOperationBuildUp(JToken parentToken, List<JToken> selectedTokens)
{
foreach (JToken selectedToken in selectedTokens)
{
if (selectedToken != null)
{
JObject parent = parentToken as JObject;
JEnumerable<JToken> copyChildren = selectedToken.Children();
if (context.IsAddOrReplacePropertiesMode())
if (Context.IsAddOrReplacePropertiesMode())
{
CopyDescendants(parent, copyChildren);
}
Expand Down Expand Up @@ -355,26 +423,25 @@ private static void AddPostOperationBuildUp(JToken parentToken, List<JToken> tok
}
}

private static void DeletePostOperationBuildUp(JToken parentToken, List<JToken> tokensToDelete, JUSTContext context)
private void DeletePostOperationBuildUp(JToken parentToken, List<JToken> tokensToDelete)
{

foreach (string selectedToken in tokensToDelete)
{
JToken tokenToRemove = GetSelectableToken(parentToken, context).Select(selectedToken);
JToken tokenToRemove = GetSelectableToken(parentToken, Context).Select(selectedToken);

if (tokenToRemove != null)
tokenToRemove.Ancestors().First().Remove();
}

}

private static void ReplacePostOperationBuildUp(JToken parentToken, Dictionary<string, JToken> tokensToReplace, JUSTContext context)
private static void ReplacePostOperationBuildUp(JToken parentToken, Dictionary<string, JToken> tokensToReplace)
{

foreach (KeyValuePair<string, JToken> tokenToReplace in tokensToReplace)
{
JsonPathSelectable selectable = JsonTransformer.GetSelectableToken(parentToken, context);
JToken selectedToken = selectable.Select(tokenToReplace.Key);
JToken selectedToken = (parentToken as JObject).SelectToken(tokenToReplace.Key);
selectedToken.Replace(tokenToReplace.Value);
}
}
Expand Down Expand Up @@ -469,15 +536,15 @@ private static void ScopePostOperationBuildUp(ref JToken parentToken, List<strin
private void LoopOperation(string propertyName, string arguments, State state, ref List<string> loopProperties, ref JArray arrayToForm, ref JObject dictToForm, JToken childToken)
{
var args = ExpressionHelper.SplitArguments(arguments, Context.EscapeChar);
var previousAlias = "root";
var previousAlias = State.RootKey;
args[0] = (string)ParseFunction(args[0], state);
_levelCounter++;
string alias = args.Length > 1 ? (string)ParseFunction(args[1].Trim(), state) : $"loop{_levelCounter}";

if (args.Length > 2)
{
previousAlias = (string)ParseFunction(args[2].Trim(), state);
state.CurrentArrayToken.Add(new LevelKey() { Key = previousAlias, Level = _levelCounter }, Context.Input);
state.CurrentArrayToken.Add(new LevelKey() { Key = alias, Level = _levelCounter }, Context.Input);
}
else if (state.CurrentArrayToken.Any(t => t.Key.Key == alias))
{
Expand Down Expand Up @@ -587,7 +654,7 @@ private bool IsArray(JToken arrayToken, string strArrayToken, State state, strin
private void ScopeOperation(string propertyName, string arguments, State state, ref List<string> scopeProperties, ref JObject scopeToForm, JToken childToken)
{
var args = ExpressionHelper.SplitArguments(arguments, Context.EscapeChar);
var previousAlias = "root";
var previousAlias = State.RootKey;
args[0] = (string)ParseFunction(args[0], state);
_levelCounter++;
string alias = args.Length > 1 ? (string)ParseFunction(args[1].Trim(), state) : $"scope{_levelCounter}";
Expand Down Expand Up @@ -937,10 +1004,10 @@ private object ParseApplyOver(IList<object> listParameters, State state)
IDictionary<LevelKey, JToken> tmpScope = new Dictionary<LevelKey, JToken>(state.CurrentScopeToken);

state.CurrentArrayToken.Clear();
state.CurrentArrayToken.Add(new LevelKey { Key = "root", Level = 0 }, input);
state.CurrentArrayToken.Add(new LevelKey { Key = State.RootKey, Level = 0 }, input);

state.CurrentScopeToken.Clear();
state.CurrentScopeToken.Add(new LevelKey { Key = "root", Level = 0 }, input);
state.CurrentScopeToken.Add(new LevelKey { Key = State.RootKey, Level = 0 }, input);

if (listParameters.ElementAt(1).ToString().Trim().Trim('\'').StartsWith("{"))
{
Expand Down Expand Up @@ -1030,7 +1097,7 @@ private object GetFunctionOutput(string functionName, IList<object> listParamete
null,
"JUST.Transformer`1",
functionName,
new[] { state.ParentArray.Single(p => p.Key.Key == alias).Value, state.CurrentArrayToken.Single(p => p.Key.Key == alias).Value }.Concat(listParameters.ToArray()).ToArray(),
new[] { state.ParentArray.First(p => p.Key.Key == alias || p.Key.Key == state.ParentArray.Last().Key.Key) .Value, state.CurrentArrayToken.Single(p => p.Key.Key == alias).Value }.Concat(listParameters.ToArray()).ToArray(),
convertParameters,
Context);
}
Expand Down Expand Up @@ -1071,11 +1138,18 @@ private object GetFunctionOutput(string functionName, IList<object> listParamete
{
if (functionName != "valueof")
{
((JUSTContext)listParameters.Last()).Input = state.CurrentArrayToken.Last().Value;
if (state.Multiple)
{
((JUSTContext)listParameters.Last()).Input = state.CurrentScopeToken.Last().Value;
}
else
{
((JUSTContext)listParameters.Last()).Input = state.CurrentArrayToken.Last().Value;
}
}
else
{
if (functionName == "valueof" && listParameters.Count > 2)
if (listParameters.Count > 2)
{
((JUSTContext)listParameters.Last()).Input = state.CurrentScopeToken.Single(p => p.Key.Key == listParameters[1].ToString()).Value;
listParameters.Remove(listParameters.ElementAt(listParameters.Count - 2));
Expand Down
14 changes: 11 additions & 3 deletions JUST.net/State.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@ internal struct LevelKey

internal sealed class State
{
internal State(JToken transformer, JToken input, int levelCounter)
internal const string RootKey = "root";
internal State(JToken transformer, JToken input, int levelCounter,
IDictionary<LevelKey, JToken> currentArrayToken = null,
IDictionary<LevelKey, JToken> currentScopeToken = null,
bool multiple = false)
{
Transformer = transformer;
ParentArray = new Dictionary<LevelKey, JArray>();
CurrentArrayToken = new Dictionary<LevelKey, JToken> { { new LevelKey { Level = levelCounter, Key = "root"}, input } };
CurrentScopeToken = new Dictionary<LevelKey, JToken> { { new LevelKey { Level = levelCounter, Key = "root"}, input } };
CurrentArrayToken = new Dictionary<LevelKey, JToken> { { new LevelKey { Level = levelCounter, Key = State.RootKey}, input } }
.Concat(currentArrayToken ?? new Dictionary<LevelKey, JToken>()).ToDictionary(p => p.Key, p => p.Value);
CurrentScopeToken = new Dictionary<LevelKey, JToken> { { new LevelKey { Level = levelCounter, Key = State.RootKey}, input } }
.Concat(currentScopeToken ?? new Dictionary<LevelKey, JToken>()).ToDictionary(p => p.Key, p => p.Value);
Multiple = multiple;
}
internal JToken Transformer { get; private set; }
internal IDictionary<LevelKey, JArray> ParentArray { get; private set; }
internal IDictionary<LevelKey, JToken> CurrentArrayToken { get; private set; }
internal IDictionary<LevelKey, JToken> CurrentScopeToken { get; private set; }
internal bool Multiple { get; private set; }

internal string GetHigherAlias()
{
Expand Down
89 changes: 86 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1682,9 +1682,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 @@ -1769,6 +1768,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
Loading