diff --git a/JUST.net/ComparisonHelper.cs b/JUST.net/ComparisonHelper.cs index 7657fff..2862697 100644 --- a/JUST.net/ComparisonHelper.cs +++ b/JUST.net/ComparisonHelper.cs @@ -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; @@ -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; } } } diff --git a/JUST.net/JsonTransformer.cs b/JUST.net/JsonTransformer.cs index 3a97c6f..a724052 100644 --- a/JUST.net/JsonTransformer.cs +++ b/JUST.net/JsonTransformer.cs @@ -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) @@ -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) { @@ -250,9 +250,77 @@ private void ParsePropertyFunction(State state, ref List 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(), + 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 tokenToForm) { if (tokenToForm != null) @@ -285,7 +353,7 @@ private void PostOperationsBuildUp(ref JToken parentToken, List tokenToF } } - private static void CopyPostOperationBuildUp(JToken parentToken, List selectedTokens, JUSTContext context) + private void CopyPostOperationBuildUp(JToken parentToken, List selectedTokens) { foreach (JToken selectedToken in selectedTokens) { @@ -293,7 +361,7 @@ private static void CopyPostOperationBuildUp(JToken parentToken, List se { JObject parent = parentToken as JObject; JEnumerable copyChildren = selectedToken.Children(); - if (context.IsAddOrReplacePropertiesMode()) + if (Context.IsAddOrReplacePropertiesMode()) { CopyDescendants(parent, copyChildren); } @@ -355,12 +423,12 @@ private static void AddPostOperationBuildUp(JToken parentToken, List tok } } - private static void DeletePostOperationBuildUp(JToken parentToken, List tokensToDelete, JUSTContext context) + private void DeletePostOperationBuildUp(JToken parentToken, List 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(); @@ -368,13 +436,12 @@ private static void DeletePostOperationBuildUp(JToken parentToken, List } - private static void ReplacePostOperationBuildUp(JToken parentToken, Dictionary tokensToReplace, JUSTContext context) + private static void ReplacePostOperationBuildUp(JToken parentToken, Dictionary tokensToReplace) { foreach (KeyValuePair 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); } } @@ -469,7 +536,7 @@ private static void ScopePostOperationBuildUp(ref JToken parentToken, List 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}"; @@ -477,7 +544,7 @@ private void LoopOperation(string propertyName, string arguments, State state, r 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)) { @@ -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 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}"; @@ -937,10 +1004,10 @@ private object ParseApplyOver(IList listParameters, State state) IDictionary tmpScope = new Dictionary(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("{")) { @@ -1030,7 +1097,7 @@ private object GetFunctionOutput(string functionName, IList 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); } @@ -1071,11 +1138,18 @@ private object GetFunctionOutput(string functionName, IList 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)); diff --git a/JUST.net/State.cs b/JUST.net/State.cs index ebeea91..00e25f3 100644 --- a/JUST.net/State.cs +++ b/JUST.net/State.cs @@ -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 currentArrayToken = null, + IDictionary currentScopeToken = null, + bool multiple = false) { Transformer = transformer; ParentArray = new Dictionary(); - CurrentArrayToken = new Dictionary { { new LevelKey { Level = levelCounter, Key = "root"}, input } }; - CurrentScopeToken = new Dictionary { { new LevelKey { Level = levelCounter, Key = "root"}, input } }; + CurrentArrayToken = new Dictionary { { new LevelKey { Level = levelCounter, Key = State.RootKey}, input } } + .Concat(currentArrayToken ?? new Dictionary()).ToDictionary(p => p.Key, p => p.Value); + CurrentScopeToken = new Dictionary { { new LevelKey { Level = levelCounter, Key = State.RootKey}, input } } + .Concat(currentScopeToken ?? new Dictionary()).ToDictionary(p => p.Key, p => p.Value); + Multiple = multiple; } internal JToken Transformer { get; private set; } internal IDictionary ParentArray { get; private set; } internal IDictionary CurrentArrayToken { get; private set; } internal IDictionary CurrentScopeToken { get; private set; } + internal bool Multiple { get; private set; } internal string GetHigherAlias() { diff --git a/README.md b/README.md index 08ab4b1..b2b853e 100644 --- a/README.md +++ b/README.md @@ -1682,9 +1682,8 @@ Output: ## 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: @@ -1769,6 +1768,90 @@ Output: } ``` +## 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 } + ] +} +``` + + ## 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. diff --git a/UnitTests/Arrays/LoopingTests.cs b/UnitTests/Arrays/LoopingTests.cs index 65ec48e..d602e93 100644 --- a/UnitTests/Arrays/LoopingTests.cs +++ b/UnitTests/Arrays/LoopingTests.cs @@ -360,7 +360,7 @@ public void LoopPathRefersAnotherLoopAlias() const string input = "{ \"orderItems\": [ { \"id\": \"1\", \"sku\": \"a\" }, { \"id\": \"2\", \"sku\": \"b\" }, { \"id\": \"3\", \"sku\": \"c\" } ], \"affectedItems\": [ 1, 2 ], \"test\": \"abc\" }"; const string transformer = "{ \"#loop($.affectedItems,affectedItems)\": { \"#loop($.orderItems,orderItems,root)\": { \"sku\": \"#currentvalueatpath($.sku)\", \"id\": \"#currentvalue(affectedItems)\" } }}"; - var result = new JsonTransformer().Transform(transformer, input); + var result = new JsonTransformer(new JUSTContext() { EvaluationMode = EvaluationMode.Strict }).Transform(transformer, input); Assert.AreEqual("[[{\"sku\":\"a\",\"id\":1},{\"sku\":\"b\",\"id\":1},{\"sku\":\"c\",\"id\":1}],[{\"sku\":\"a\",\"id\":2},{\"sku\":\"b\",\"id\":2},{\"sku\":\"c\",\"id\":2}]]", result); } diff --git a/UnitTests/MultipleTransformations.cs b/UnitTests/MultipleTransformations.cs new file mode 100644 index 0000000..fbfb59b --- /dev/null +++ b/UnitTests/MultipleTransformations.cs @@ -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')])\" ] } }"; + + var result = new JsonTransformer(new JUSTContext() { EvaluationMode = EvaluationMode.Strict}).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(new JUSTContext() { EvaluationMode = EvaluationMode.Strict}).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(new JUSTContext() { EvaluationMode = EvaluationMode.Strict}).Transform(transformer, input); + + Assert.AreEqual("{\"loop\":[{\"result\":true},{\"result\":false}]}", result); + } + } +} \ No newline at end of file diff --git a/UnitTests/ReadmeTests.cs b/UnitTests/ReadmeTests.cs index 45ef736..fe03105 100644 --- a/UnitTests/ReadmeTests.cs +++ b/UnitTests/ReadmeTests.cs @@ -268,6 +268,17 @@ public void UsePreviousGeneratedProperty() Assert.AreEqual("{\"first\":123,\"second\":123,\"third\":125,\"fourth\":true,\"fifth\":true,\"sixth\":\"value is true\"}", 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(new JUSTContext { EvaluationMode = EvaluationMode.Strict }).Transform(transformer, input); + + Assert.AreEqual("{\"scalar\":true,\"object\":{\"result\":true},\"select_token\":{\"result\":true},\"loop\":[{\"result\":true},{\"result\":false}]}", result); + } + [Test] public void Scopes() {