From 62a56cf3113531eb97d97a1a5c0ba6ce04e47bc3 Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 14:00:17 -0700 Subject: [PATCH 1/7] Removing unnecessary entry point Add test logger --- .../JsonTransformationTestLogger.cs | 114 ++++++++++++++++++ .../IJsonTransformationLogger.cs | 9 -- .../JsonTransformation.cs | 5 +- 3 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs new file mode 100644 index 00000000..20163779 --- /dev/null +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Jdt.Tests +{ + using System; + using System.Text; + + /// + /// Mock logger to test + /// + public class JsonTransformationTestLogger : IJsonTransformationLogger + { + private readonly StringBuilder errorLog = new StringBuilder(); + + private readonly StringBuilder warningLog = new StringBuilder(); + + private readonly StringBuilder messageLog = new StringBuilder(); + + /// + /// Gets the text from the error log + /// + public string ErrorLogText + { + get + { + return this.errorLog.ToString(); + } + } + + /// + /// Gets the text from the warning log + /// + public string WarningLogText + { + get + { + return this.warningLog.ToString(); + } + } + + /// + /// Gets the text from the message log + /// + public string MessageLogText + { + get + { + return this.messageLog.ToString(); + } + } + + /// + public void LogError(string message) + { + this.errorLog.AppendLine(message); + } + + /// + public void LogError(string message, string fileName, int lineNumber, int linePosition) + { + this.errorLog.AppendLine(this.BuildLine(message, fileName, lineNumber, linePosition)); + } + + /// + public void LogErrorFromException(Exception ex) + { + this.errorLog.AppendLine($"Exception: {ex.Message}"); + } + + /// + public void LogErrorFromException(Exception ex, string fileName, int lineNumber, int linePosition) + { + this.errorLog.AppendLine(this.BuildLine($"Exception: {ex.Message}", fileName, lineNumber, linePosition)); + } + + /// + public void LogMessage(string message) + { + this.messageLog.AppendLine(message); + } + + /// + public void LogWarning(string message) + { + this.warningLog.AppendLine(message); + } + + /// + public void LogWarning(string message, string fileName) + { + this.warningLog.AppendLine($"{message} {fileName}"); + } + + /// + public void LogWarning(string message, string fileName, int lineNumber, int linePosition) + { + this.warningLog.AppendLine(this.BuildLine(message, fileName, lineNumber, linePosition)); + } + + private string BuildLine(string message, string fileName, int lineNumber, int linePosition) + { + string line = message; + if (fileName != null) + { + line += " " + fileName; + } + + line += $" {lineNumber} {linePosition}"; + + return line; + } + } +} diff --git a/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs b/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs index bfcacd28..968f053b 100644 --- a/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs +++ b/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs @@ -16,15 +16,6 @@ public interface IJsonTransformationLogger /// The message text void LogMessage(string message); - /// - /// Logs a message - /// - /// The message - /// The full path to the file that caused the message. Can be null - /// The line that caused the message - /// The position in the line that caused the message - void LogMessage(string message, string fileName, int lineNumber, int linePosition); - /// /// Logs a warning /// diff --git a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs index b2def301..a4938c43 100644 --- a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs +++ b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs @@ -162,10 +162,7 @@ private void SetTransform(Stream transformStream) this.loadSettings = new JsonLoadSettings() { CommentHandling = CommentHandling.Ignore, - - // Obs: LineInfo is handled on Ignore and not Load - // See https://github.com/JamesNK/Newtonsoft.Json/issues/1249 - LineInfoHandling = LineInfoHandling.Ignore + LineInfoHandling = LineInfoHandling.Load }; using (StreamReader transformStreamReader = new StreamReader(transformStream)) From ff0b69016d5ffbb1a561b92cdf6881a0933a7132 Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 16:21:33 -0700 Subject: [PATCH 2/7] Adding tests Changing test logging Fixing clone to carry over lineinfo --- doc/jdt_spec.md | 619 ++++++++++++++++++ .../JsonTransformationTest.cs | 302 +++++++++ .../JsonTransformationTestLogger.cs | 106 +-- .../JdtExtensions.cs | 18 + .../Processors/JdtProcessor.cs | 2 +- 5 files changed, 999 insertions(+), 48 deletions(-) create mode 100644 doc/jdt_spec.md create mode 100644 src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs diff --git a/doc/jdt_spec.md b/doc/jdt_spec.md new file mode 100644 index 00000000..7be7dd9c --- /dev/null +++ b/doc/jdt_spec.md @@ -0,0 +1,619 @@ +# JSON Document Transforms + +The JDT language aims to provide simple, intuitive transformations for JSON files while keeping the transformation file as close to the original file as possible. JDT also provides more complex behavior through specific transformations with special syntax outlining the desired result. + +### Summary + +JSON document transformations seek to change a single JSON file (source) based on transformations specified in another JSON file (transform), generating a new JSON (result). The default behavior of JDT is to merge the transformation file into the source file. More advanced behavior can be specified by the user through the defined JDT syntax. + +## Default Transformation + +If no behavior is specified in any JSON object, the default transformation is to merge the two files. The goal of JDT is to have a default behavior that satisfies most user scenarios. For more specifics on merging, see the Merge transformation. + +#### Example + +Source: +``` javascript +{ + "Version": 1, + "Settings": { + "Setting01" : "Default01", + "Setting02" : "Default02" + }, + "SupportedVersions" : [1, 2, 3] +} +``` + +Transform: +``` javascript +{ + "Version": 2, + "Settings": { + "Setting01" : "NewValue01", + "Setting03" : "NewValue03" + }, + "SupportedVersions" : [4, 5], + "UseThis" : true +} +``` + +Result: +``` javascript +{ + // Overriden by the transformation file + "Version": 2, + "Settings": { + // Overriden by the transformation file + "Setting01" : "NewValue01", + // Not present in the transformation file, unchanged + "Setting02" : "Default02", + // Added by the transformation file + "Setting03" : "NewValue03" + }, + // The array in the transformation file was appended + "SupportedVersions" : [1, 2, 3, 4, 5], + // Added by the transformation file + "UseThis" : true +} +``` + +## Attributes + +Attributes are advanced methods to specify behaviors that cannot be achieved through the default transformation. Any transformation can be an object containing valid attributes for that transformation. Attributes outside of transformations are not allowed and generate errors. + +### Path + +Use JSONPath syntax to navigate to the node where the transform should be applied. + + +| Use: | `"@jdt.path" : ` (Case sensitive) | +| ---- |:----------------------------------------:| + +Value must be a string with a valid JSONPath. Any other type generates an error. If the path does not match any nodes, the transformation is not performed. + +All JDT Paths are relative to the node that they're in. + +For more information on JSONPath syntax, see [here](http://goessner.net/articles/JsonPath/index.html) + +### Value + +The transformation value that should be applied. + +| Use: | `"@jdt.value" : ` (Case sensitive) | +| ---- |:-----------------------------------------:| + +Value depends on the syntax of the transformation verb. See Transformation Verbs. + +## Transformation Verbs + +Verbs are used within the transformation file to specify specific transformations that should be executed. This helps perform actions that are not covered by the default transformation, such as entirely replacing an existing node. + +A transformation verb always applies to the values of the current node and never the key. + +### Rename + +| Use: | `"@jdt.rename" : ` (Case sensitive) | +| ---- |:------------------------------------------:| + + +| Value Type: | Behavior | +| ----------- | ------------------------------- | +| Primitive | Not allowed. Generates an error | +| Object | If the object contains JDT attributes, apply them.
See Attributes. If not, it must only contain key-value pairs where the key is the name of the node that should be renamed and the value is a string with the new name. +| Array | Applies rename with each element of the array as the transformation value.
If the transformation value is an array, generate an error. + +**Obs:** Renaming the root node is not allowed and will generate an error. + +#### Example + +Source: +``` javascript +{ + "A" : { + "A1" : 11, + "A2" : { + "A21" : 121, + "A22" : 122 + } + }, + "B" : [ + 21, + 22 + ], + "C" : 3 +} +``` + +Transform: +``` javascript +{ + "@jdt.rename" : { + "A" : "Astar", + "B" : "Bstar" + } +} +``` + +Result: +``` javascript +{ + // Does not alter result + "Astar" : { + "A1" : 11, + "A2" : { + "A21" : 121, + "A22" : 122 + } + }, + // Does not depend on object type + "Bstar" : [ + 21, + 22 + ], + // Does not alter siblings + "C" : 3 +} +``` + +#### Path Attribute + +The `@jdt.path` attribute can be used to specify a node to rename. Absolute or relative paths can be specified to the node. Renaming elements of arrays is not supported and should generate an error. + +Source: +``` javascript +{ + "A" : { + "RenameThis" : true + }, + "B" : { + "RenameThis" : false + }, + "C" : [ + { + "Name" : "C01", + "Value" : 1 + }, + { + "Name" : "C02", + "Value" : 2 + } + ] +} +``` + +Transform: +``` javascript +{ + "@jdt.rename" : { + "@jdt.Path " : "$[?(@.Rename == true)]", + "@jdt.Value" : "Astar" + }, + "C" : { + "@jdt.rename" : { + "@jdt.path" : "@[*].Name", + "@jdt.value" : "Nstar" + } + } +} +``` + +Result: +``` javascript +{ + // Only this node matches the path + "Astar" : { + "RenameThis" : true + }, + "B" : { + "RenameThis" : false + }, + // Renaming nodes from an object + // in the array is allowed + "C" : [ + { + "Nstar" : "C01", + "Value" : 1 + }, + { + "Nstar" : "C02", + "Value" : 2 + } + ] +} +``` +### Remove + +| Use: | `"@jdt.remove" : ` (Case sensitive) | +| ---- |:------------------------------------------:| + + +| Value Type: | Behavior | +| ------------ | ----------------------------------------------------------- | +| String | Removes the node with the given name from the current level | +| Boolean | If true, remove all the nodes from the current level and sets value to null. If false, do nothing +| Number, null | Not allowed. Generates an error. +| Object | If the object contains JDT attributes, apply them. See Attributes.
If not, generate error. +| Array | Applies remove with each element of the array as the transformation value.
If the transformation value is an array, generate an error. + + +**Obs:** The `@jdt.value` attribute cannot be used with this transformation + +#### Example + +Source: +``` javascript +{ + "A" : 1, + "Astar" : 10, + "B" : 2, + "C" : { + "C1" : 31, + "C2" : 32 + }, + "D" : { + "D1" : 41, + "D2" : 42, + "D3" : 43 + } +} +``` + +Transform: +``` javascript +{ + "@jdt.remove" : "Astar", + "C" : { + "@jdt.remove" : true + }, + "D" : { + "@jdt.remove" : ["D2", "D3"] + } +} +``` + +Result: +``` javascript +{ + // Astar is completely removed + "A" : 1, + "B" : 2, + // All nodes are removed + "C" : null, + "D" : { + "D1" : 41 + // Multiple nodes were removed + } +} +``` + +#### Path Attribute + +The `@jdt.path` attribute can be used to specify the absolute or relative path to the nodes that should be removed. It can also be used to remove elements from arrays. If the Path attribute is present, the Value attribute is not supported and is ignored if present in the transformation. + +Source: +``` javascript +{ + "A" : { + "RemoveThis" : true + }, + "B" : { + "RemoveThis" : false + }, + "C" : { + "C1" : 1, + "C2" : { + "C21" : 21 + } + } +} +``` + +Transform: +``` javascript +{ + //Remove only matching nodes from this level + "@jdt.remove" : { + "@jdt.path" : "$[?(@.RemoveThis == true)]" + }, + "C" : { + //Specify a relative path to the node + "@jdt.remove" : { + "@jdt.path" : "@.C2.C21" + } + } +} +``` + +Result: +``` javascript +{ + "B" : { + "RemoveThis" : false + }, + "C" : { + "C1" : 1, + "C2" : { + } + } +} +``` + +### Merge + +| Use: | `"@jdt.merge" : ` (Case sensitive) | +| ---- |:-----------------------------------------:| + + +| Value Type: | Behavior | +| ----------- | ----------------------------------------------------------- | +| Primitive | Replaces the value of the current node with the given value | +| Object | Recursively merges the object into the current node. Keys that are not present in the source file will be added.
If the object contains JDT attributes, apply them. See Attributes. +| Array | Applies merge with each element of the array as the transformation value.
In an explicit merge, if the transformation value should be the array, double brackets should be used (e.g. `[[]]`). In a default transformation, this is not necessary. + +**Obs:** If the transformation value does not match the source value for an already existing node, the transformation value will replace the existing one. + +#### Path Attribute + +The `@jdt.path` attribute can be used if a specific node or multiple nodes should be changed. It can also be used to change nodes within arrays. See Attributes for more information. + +Source: +``` javascript +{ + "A": { + "TransformThis": true + }, + "B": { + "TransformThis": false + }, + "C": { + }, + "D": { + "TransformThis": "WrongValue" + }, + "E": { + "TransformThis": false, + "Items": [ + { + "Value": 10 + }, + { + "Value": 20 + }, + { + "Value": 30 + } + ] + } +} +``` + +Transform: +``` javascript +{ + //Executes for all nodes on this level + "@jdt.merge" : [{ + "@jdt.path" : "$.*", + "@jdt.value" : { + "Default" : 0 + } + }, + //This only executes for matching nodes + { + "@jdt.path" : "$[?(@.TransformThis == true)]", + "@jdt.Value" : { + "Transformed" : true + } + }], + "E": { + // Accessing objects in array + "@jdt.merge" : { + "@jdt.path" : "$.Items[?(@.Value < 15)]", + "@jdt.value" : { + "Value" : 15, + "Changed" : true + } + } + } +} +``` + +Result: +``` javascript +{ + "A": { + "TransformThis" : true, + "Default" : 0, + "Transformed" : true + }, + "B": { + "TransformThis": false, + "Default" : 0 + }, + "C": { + "Default" : 0 + }, + "D": { + "TransformThis": "WrongValue", + "Default" : 0 + }, + "E": { + "TransformThis": false, + "Items": [ + { + "Value" : 15, + "Changed" : true + }, + { + "Value": 20 + }, + { + "Value": 30 + } + ], + "Default" : 0 + } +} +``` + +#### Value Attribute + +The `@jdt.value` attribute in a Merge is the only type that supports nested transformations. This means that transformations that should be executed in newly created or merged nodes can be added through this value. + +### Replace + +| Use: | `"@jdt.replace" : ` (Case sensitive) | +| ---- |:-------------------------------------------:| + + +| Value Type: | Behavior | +| ----------- | ----------------------------------------------------------- | +| Primitive | Replaces the current node with the given value | +| Object | If the object contains JDT attributes, apply them. See Attributes.
If not, replaces the node with the given object. +| Array | Applies merge with each element of the array as the transformation value.
If the transformation value is an array, replace the node with the given array. + +#### Example + +Source: +``` javascript +{ + "A" : { + "A1" : "11" + }, + "B" : { + "1B" : 12, + "2B" : 22 + }, + "C" : { + "C1" : 31, + "C2" : 32 + } +} +``` + +Transform: +``` javascript +{ + "A": { + "@jdt.replace": 1 + }, + "B": { + "@jdt.replace": { + "B1": 11, + "B2": 12 + } + }, + "C": { + // Double brackets are needed to specify + // the array as the transformation value + "@jdt.replace": [[ + { + "Value": 31 + }, + { + "Value": 32 + } + ]] + } +} +``` + +Result: +``` javascript +{ + "A" : 1, + "B" : { + "B1" : 11, + "B2" : 12 + }, + "C" : [ + { + "Value": 31 + }, + { + "Value": 32 + } + ] +} +``` + +#### Path Attribute + +Source: +``` javascript +{ + "A" : { + "A1" : 11, + "A2" : "Replace" + }, + "B" : [ + { + "ReplaceThis" :true + }, + { + "ReplaceThis" : false + } + ] +} +``` + +Transform: +``` javascript +{ + "@jdt.replace" : { + "@jdt.path" : "$.A.A2", + "@jdt.value" : 12 + }, + "B" : { + "@jdt.replace" : { + "@jdt.path" : "@[?(@.ReplaceThis == true)]", + "@jdt.value" : { + "Replaced" : true + } + } + } +} +``` + +Result: +``` javascript +{ + "A" : { + "A1" : 11, + "A2" : 12 + }, + "B" : [ + { + // The entire object was replaced + "Replaced" : true + }, + { + "ReplaceThis" : false + } + ] +} +``` + +## Order of Execution + +Transformations should be done in depth-first order to guarantee that everything is executed. Breadth-first order would have a slightly faster execution time as Remove transformations would potentially exclude other transformations from child nodes. For the same reason, this ordering could exclude transformations on lower level nodes that a user would like to execute. + +In the same level, the order of priority for transformation is as follows: + +Remove > Replace > Merge > Default > Rename + +This order guarantees that explicitly named transforms execute first. It also guarantees that removals prevent unnecessary transformations from occurring. + +### Processing Transform Files + +To guarantee the order of execution defined previously, the transformed file is processed in the following order. + +Starting from the root node: + +1) Enqueue all JDT transformations, identified by @jdt verbs +2) Iterate through the rest of the node + 1. Object + 1. If it exists in the original file: step into the object and start from step 1 + 2. If it does not exist, enqueue a merge transformation + 2. Array: Enqueue a merge transformation + 3. Primitive: Enqueue a merge (replace) transformation +3) Process all the transformation in the queue, per the order of execution diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs new file mode 100644 index 00000000..ae2f4562 --- /dev/null +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Jdt.Tests +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using Xunit; + + /// + /// Test class for + /// + public class JsonTransformationTest + { + private static readonly string SimpleSourceString = @"{ 'A': 1 }"; + + private readonly JsonTransformationTestLogger logger; + + /// + /// Initializes a new instance of the class. + /// + public JsonTransformationTest() + { + // xUnit creates a new instance of the class for each test, so a new logger is created + this.logger = new JsonTransformationTestLogger(); + } + + /// + /// Tests the error caused when an invalid verb is found + /// + [Fact] + public void InvalidVerb() + { + string transformString = @"{ + '@jdt.invalid': false + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 56, true); + } + + /// + /// Tests the error caused by a verb having an invalid value + /// + [Fact] + public void InvalidVerbValue() + { + string transformString = @"{ + '@jdt.remove': 10 + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 58, true); + } + + /// + /// Tests the error caused when an invalid attribute is found within a verb + /// + [Fact] + public void InvalidAttribute() + { + string transformString = @"{ + '@jdt.replace': { + '@jdt.invalid': false + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 58, true); + } + + /// + /// Tests the error caused when a required attribute is not found + /// + [Fact] + public void MissingAttribute() + { + string transformString = @"{ + '@jdt.rename': { + '@jdt.path': 'A' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); + } + + /// + /// Tests the error caused when a verb object contains attributes and other objects + /// + [Fact] + public void MixedAttributes() + { + string transformString = @"{ + '@jdt.rename': { + '@jdt.path': 'A', + '@jdt.value': 'Astar', + 'NotAttribute': true + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); + } + + /// + /// Tests the error caused when an attribute has an incorrect value + /// + [Fact] + public void WrongAttributeValue() + { + string transformString = @"{ + '@jdt.remove': { + '@jdt.path': false + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 61, true); + } + + /// + /// Tests the error caused when a path attribute returns no result + /// + [Fact] + public void RemoveNonExistantNode() + { + string transformString = @"{ + '@jdt.remove': { + '@jdt.path': 'B' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, true); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.ErrorLog); + LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 59, false); + } + + /// + /// Tests the error caused when attempting to remove the root node + /// + [Fact] + public void RemoveRoot() + { + string transformString = @"{ + '@jdt.remove': true + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 60, true); + } + + /// + /// Tests the error when a rename value is invalid + /// + [Fact] + public void InvalidRenameValue() + { + string transformString = @"{ + '@jdt.rename': { + 'A': 10 + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 47, true); + } + + /// + /// Tests the error caused when attempting to rename a non-existant node + /// + [Fact] + public void RenameNonExistantNode() + { + string transformString = @"{ + '@jdt.rename': { + 'B': 'Bstar' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, true); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.ErrorLog); + LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 47, false); + } + + /// + /// Test the error when attempting to replace the root with a non-object token + /// + [Fact] + public void ReplaceRoot() + { + string transformString = @"{ + '@jdt.replace': 10 + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 59, true); + } + + /// + /// Tests that an exception is thrown when is called + /// + [Fact] + public void ThrowAndLogException() + { + string transformString = @"{ + '@jdt.invalid': false + }"; + using (var transformStream = this.GetStreamFromString(transformString)) + using (var sourceStream = this.GetStreamFromString(SimpleSourceString)) + { + JsonTransformation transform = new JsonTransformation(transformStream, this.logger); + var exception = Record.Exception(() => transform.Apply(sourceStream)); + Assert.NotNull(exception); + Assert.IsType(exception); + var jdtException = exception as JdtException; + Assert.Contains("invalid", jdtException.Message); + Assert.Equal(ErrorLocation.Transform, jdtException.Location); + Assert.Equal(2, jdtException.LineNumber); + Assert.Equal(56, jdtException.LinePosition); + } + } + + private static void LogHasSingleEntry(List log, string fileName, int lineNumber, int linePosition, bool fromException) + { + Assert.Single(log); + var errorEntry = log.Single(); + Assert.Equal(fileName, errorEntry.FileName); + Assert.Equal(lineNumber, errorEntry.LineNumber); + Assert.Equal(linePosition, errorEntry.LinePosition); + Assert.Equal(fromException, errorEntry.FromException); + } + + private void TryTransformTest(string sourceString, string transformString, bool shouldTransformSucceed) + { + using (var transformStream = this.GetStreamFromString(transformString)) + using (var sourceStream = this.GetStreamFromString(sourceString)) + { + JsonTransformation transform = new JsonTransformation(transformStream, this.logger); + Stream result = null; + + var exception = Record.Exception(() => result = transform.Apply(sourceStream)); + + if (shouldTransformSucceed) + { + Assert.NotNull(result); + Assert.Null(exception); + } + else + { + Assert.Null(result); + Assert.NotNull(exception); + Assert.IsType(exception); + } + } + } + + private Stream GetStreamFromString(string s) + { + MemoryStream stringStream = new MemoryStream(); + StreamWriter stringWriter = new StreamWriter(stringStream); + stringWriter.Write(s); + stringWriter.Flush(); + stringStream.Position = 0; + + return stringStream; + } + } +} diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs index 20163779..50d0c017 100644 --- a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs @@ -4,111 +4,123 @@ namespace Microsoft.VisualStudio.Jdt.Tests { using System; - using System.Text; + using System.Collections.Generic; /// /// Mock logger to test /// public class JsonTransformationTestLogger : IJsonTransformationLogger { - private readonly StringBuilder errorLog = new StringBuilder(); - - private readonly StringBuilder warningLog = new StringBuilder(); - - private readonly StringBuilder messageLog = new StringBuilder(); - /// - /// Gets the text from the error log + /// Gets the error log /// - public string ErrorLogText - { - get - { - return this.errorLog.ToString(); - } - } + public List ErrorLog { get; } = new List(); /// - /// Gets the text from the warning log + /// Gets the warning log /// - public string WarningLogText - { - get - { - return this.warningLog.ToString(); - } - } + public List WarningLog { get; } = new List(); /// - /// Gets the text from the message log + /// Gets the message log /// - public string MessageLogText - { - get - { - return this.messageLog.ToString(); - } - } + public List MessageLog { get; } = new List(); /// public void LogError(string message) { - this.errorLog.AppendLine(message); + this.ErrorLog.Add(new TestLogEntry(message, null, 0, 0, false)); } /// public void LogError(string message, string fileName, int lineNumber, int linePosition) { - this.errorLog.AppendLine(this.BuildLine(message, fileName, lineNumber, linePosition)); + this.ErrorLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); } /// public void LogErrorFromException(Exception ex) { - this.errorLog.AppendLine($"Exception: {ex.Message}"); + this.ErrorLog.Add(new TestLogEntry(ex.Message, null, 0, 0, true)); } /// public void LogErrorFromException(Exception ex, string fileName, int lineNumber, int linePosition) { - this.errorLog.AppendLine(this.BuildLine($"Exception: {ex.Message}", fileName, lineNumber, linePosition)); + this.ErrorLog.Add(new TestLogEntry(ex.Message, fileName, lineNumber, linePosition, true)); } /// public void LogMessage(string message) { - this.messageLog.AppendLine(message); + this.MessageLog.Add(new TestLogEntry(message, null, 0, 0, false)); } /// public void LogWarning(string message) { - this.warningLog.AppendLine(message); + this.WarningLog.Add(new TestLogEntry(message, null, 0, 0, false)); } /// public void LogWarning(string message, string fileName) { - this.warningLog.AppendLine($"{message} {fileName}"); + this.WarningLog.Add(new TestLogEntry(message, fileName, 0, 0, false)); } /// public void LogWarning(string message, string fileName, int lineNumber, int linePosition) { - this.warningLog.AppendLine(this.BuildLine(message, fileName, lineNumber, linePosition)); + this.WarningLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); } - private string BuildLine(string message, string fileName, int lineNumber, int linePosition) + /// + /// An test entry for the logger. + /// Corresponds to an error, warning or message + /// + public struct TestLogEntry { - string line = message; - if (fileName != null) + /// + /// The log message + /// + public string Message; + + /// + /// The file that caused the entry + /// + public string FileName; + + /// + /// The line in the file + /// + public int LineNumber; + + /// + /// The position in the line + /// + public int LinePosition; + + /// + /// Whether the entry was caused from an exception + /// + public bool FromException; + + /// + /// Initializes a new instance of the struct. + /// + /// The entry message + /// The file that caused the entry + /// The line in the file + /// The position in the line + /// Whether the entry was caused by an exception + public TestLogEntry(string message, string file, int lineNumber, int linePosition, bool fromException) { - line += " " + fileName; + this.Message = message; + this.FileName = file; + this.LineNumber = lineNumber; + this.LinePosition = linePosition; + this.FromException = fromException; } - - line += $" {lineNumber} {linePosition}"; - - return line; } } } diff --git a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs index a764b0f3..d95aa3e2 100644 --- a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs +++ b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs @@ -45,5 +45,23 @@ internal static bool IsCriticalException(this Exception ex) #endif ; } + + /// + /// Clones a preserving the line information + /// + /// The object to clone + /// A clone of the object with its line info + internal static JObject CloneWithLineInfo(this JObject objectToClone) + { + var loadSettings = new JsonLoadSettings() + { + LineInfoHandling = LineInfoHandling.Load + }; + + using (var objectReader = objectToClone.CreateReader()) + { + return JObject.Load(objectReader, loadSettings); + } + } } } diff --git a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs index ee51422a..68138f53 100644 --- a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs +++ b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs @@ -70,7 +70,7 @@ internal static void ProcessTransform(JObject source, JObject transform, JsonTra } // Passes in a clone of the transform object because it can be altered during the transformation process - ProcessorChain.Start(source, (JObject)transform.DeepClone(), logger); + ProcessorChain.Start(source, (JObject)transform.CloneWithLineInfo(), logger); } /// From b8467f9ecc36c4a1db995758e608c822528b68fc Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 16:31:17 -0700 Subject: [PATCH 3/7] Adding comments --- .../JsonTransformationTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs index ae2f4562..9b360a8c 100644 --- a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs @@ -41,6 +41,8 @@ public void InvalidVerb() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error should be where at the location of the invalid verb LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 56, true); } @@ -58,6 +60,8 @@ public void InvalidVerbValue() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the location of the invalid value LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 58, true); } @@ -77,6 +81,8 @@ public void InvalidAttribute() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the invalid attribute LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 58, true); } @@ -96,6 +102,8 @@ public void MissingAttribute() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the beginning of the object with the missing attribute LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); } @@ -117,6 +125,8 @@ public void MixedAttributes() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the beginning of the object with the mixed attribute LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); } @@ -136,6 +146,8 @@ public void WrongAttributeValue() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the invalid value LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 61, true); } @@ -155,6 +167,8 @@ public void RemoveNonExistantNode() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.ErrorLog); + + // The warning location should be at the position of the path value that yielded no results LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 59, false); } @@ -172,6 +186,8 @@ public void RemoveRoot() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the remove value LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 60, true); } @@ -191,6 +207,8 @@ public void InvalidRenameValue() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the rename property LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 47, true); } @@ -210,6 +228,8 @@ public void RenameNonExistantNode() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.ErrorLog); + + // The position of the warning should be a the beginning of the rename property LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 47, false); } @@ -227,6 +247,8 @@ public void ReplaceRoot() Assert.Empty(this.logger.MessageLog); Assert.Empty(this.logger.WarningLog); + + // The position of the error should be at the value of replace that caused it LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 59, true); } From 41235071c773d1888f71c89ef4a818f2ce0b36d5 Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 16:33:56 -0700 Subject: [PATCH 4/7] Remove using --- src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs index 9b360a8c..42037dfd 100644 --- a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs @@ -6,7 +6,6 @@ namespace Microsoft.VisualStudio.Jdt.Tests using System.Collections.Generic; using System.IO; using System.Linq; - using System.Text; using Xunit; /// From 4cc2d52875644ecff534257387cb573ac60b5e5b Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 16:38:49 -0700 Subject: [PATCH 5/7] Remove jdt spec file --- doc/jdt_spec.md | 619 ------------------------------------------------ 1 file changed, 619 deletions(-) delete mode 100644 doc/jdt_spec.md diff --git a/doc/jdt_spec.md b/doc/jdt_spec.md deleted file mode 100644 index 7be7dd9c..00000000 --- a/doc/jdt_spec.md +++ /dev/null @@ -1,619 +0,0 @@ -# JSON Document Transforms - -The JDT language aims to provide simple, intuitive transformations for JSON files while keeping the transformation file as close to the original file as possible. JDT also provides more complex behavior through specific transformations with special syntax outlining the desired result. - -### Summary - -JSON document transformations seek to change a single JSON file (source) based on transformations specified in another JSON file (transform), generating a new JSON (result). The default behavior of JDT is to merge the transformation file into the source file. More advanced behavior can be specified by the user through the defined JDT syntax. - -## Default Transformation - -If no behavior is specified in any JSON object, the default transformation is to merge the two files. The goal of JDT is to have a default behavior that satisfies most user scenarios. For more specifics on merging, see the Merge transformation. - -#### Example - -Source: -``` javascript -{ - "Version": 1, - "Settings": { - "Setting01" : "Default01", - "Setting02" : "Default02" - }, - "SupportedVersions" : [1, 2, 3] -} -``` - -Transform: -``` javascript -{ - "Version": 2, - "Settings": { - "Setting01" : "NewValue01", - "Setting03" : "NewValue03" - }, - "SupportedVersions" : [4, 5], - "UseThis" : true -} -``` - -Result: -``` javascript -{ - // Overriden by the transformation file - "Version": 2, - "Settings": { - // Overriden by the transformation file - "Setting01" : "NewValue01", - // Not present in the transformation file, unchanged - "Setting02" : "Default02", - // Added by the transformation file - "Setting03" : "NewValue03" - }, - // The array in the transformation file was appended - "SupportedVersions" : [1, 2, 3, 4, 5], - // Added by the transformation file - "UseThis" : true -} -``` - -## Attributes - -Attributes are advanced methods to specify behaviors that cannot be achieved through the default transformation. Any transformation can be an object containing valid attributes for that transformation. Attributes outside of transformations are not allowed and generate errors. - -### Path - -Use JSONPath syntax to navigate to the node where the transform should be applied. - - -| Use: | `"@jdt.path" : ` (Case sensitive) | -| ---- |:----------------------------------------:| - -Value must be a string with a valid JSONPath. Any other type generates an error. If the path does not match any nodes, the transformation is not performed. - -All JDT Paths are relative to the node that they're in. - -For more information on JSONPath syntax, see [here](http://goessner.net/articles/JsonPath/index.html) - -### Value - -The transformation value that should be applied. - -| Use: | `"@jdt.value" : ` (Case sensitive) | -| ---- |:-----------------------------------------:| - -Value depends on the syntax of the transformation verb. See Transformation Verbs. - -## Transformation Verbs - -Verbs are used within the transformation file to specify specific transformations that should be executed. This helps perform actions that are not covered by the default transformation, such as entirely replacing an existing node. - -A transformation verb always applies to the values of the current node and never the key. - -### Rename - -| Use: | `"@jdt.rename" : ` (Case sensitive) | -| ---- |:------------------------------------------:| - - -| Value Type: | Behavior | -| ----------- | ------------------------------- | -| Primitive | Not allowed. Generates an error | -| Object | If the object contains JDT attributes, apply them.
See Attributes. If not, it must only contain key-value pairs where the key is the name of the node that should be renamed and the value is a string with the new name. -| Array | Applies rename with each element of the array as the transformation value.
If the transformation value is an array, generate an error. - -**Obs:** Renaming the root node is not allowed and will generate an error. - -#### Example - -Source: -``` javascript -{ - "A" : { - "A1" : 11, - "A2" : { - "A21" : 121, - "A22" : 122 - } - }, - "B" : [ - 21, - 22 - ], - "C" : 3 -} -``` - -Transform: -``` javascript -{ - "@jdt.rename" : { - "A" : "Astar", - "B" : "Bstar" - } -} -``` - -Result: -``` javascript -{ - // Does not alter result - "Astar" : { - "A1" : 11, - "A2" : { - "A21" : 121, - "A22" : 122 - } - }, - // Does not depend on object type - "Bstar" : [ - 21, - 22 - ], - // Does not alter siblings - "C" : 3 -} -``` - -#### Path Attribute - -The `@jdt.path` attribute can be used to specify a node to rename. Absolute or relative paths can be specified to the node. Renaming elements of arrays is not supported and should generate an error. - -Source: -``` javascript -{ - "A" : { - "RenameThis" : true - }, - "B" : { - "RenameThis" : false - }, - "C" : [ - { - "Name" : "C01", - "Value" : 1 - }, - { - "Name" : "C02", - "Value" : 2 - } - ] -} -``` - -Transform: -``` javascript -{ - "@jdt.rename" : { - "@jdt.Path " : "$[?(@.Rename == true)]", - "@jdt.Value" : "Astar" - }, - "C" : { - "@jdt.rename" : { - "@jdt.path" : "@[*].Name", - "@jdt.value" : "Nstar" - } - } -} -``` - -Result: -``` javascript -{ - // Only this node matches the path - "Astar" : { - "RenameThis" : true - }, - "B" : { - "RenameThis" : false - }, - // Renaming nodes from an object - // in the array is allowed - "C" : [ - { - "Nstar" : "C01", - "Value" : 1 - }, - { - "Nstar" : "C02", - "Value" : 2 - } - ] -} -``` -### Remove - -| Use: | `"@jdt.remove" : ` (Case sensitive) | -| ---- |:------------------------------------------:| - - -| Value Type: | Behavior | -| ------------ | ----------------------------------------------------------- | -| String | Removes the node with the given name from the current level | -| Boolean | If true, remove all the nodes from the current level and sets value to null. If false, do nothing -| Number, null | Not allowed. Generates an error. -| Object | If the object contains JDT attributes, apply them. See Attributes.
If not, generate error. -| Array | Applies remove with each element of the array as the transformation value.
If the transformation value is an array, generate an error. - - -**Obs:** The `@jdt.value` attribute cannot be used with this transformation - -#### Example - -Source: -``` javascript -{ - "A" : 1, - "Astar" : 10, - "B" : 2, - "C" : { - "C1" : 31, - "C2" : 32 - }, - "D" : { - "D1" : 41, - "D2" : 42, - "D3" : 43 - } -} -``` - -Transform: -``` javascript -{ - "@jdt.remove" : "Astar", - "C" : { - "@jdt.remove" : true - }, - "D" : { - "@jdt.remove" : ["D2", "D3"] - } -} -``` - -Result: -``` javascript -{ - // Astar is completely removed - "A" : 1, - "B" : 2, - // All nodes are removed - "C" : null, - "D" : { - "D1" : 41 - // Multiple nodes were removed - } -} -``` - -#### Path Attribute - -The `@jdt.path` attribute can be used to specify the absolute or relative path to the nodes that should be removed. It can also be used to remove elements from arrays. If the Path attribute is present, the Value attribute is not supported and is ignored if present in the transformation. - -Source: -``` javascript -{ - "A" : { - "RemoveThis" : true - }, - "B" : { - "RemoveThis" : false - }, - "C" : { - "C1" : 1, - "C2" : { - "C21" : 21 - } - } -} -``` - -Transform: -``` javascript -{ - //Remove only matching nodes from this level - "@jdt.remove" : { - "@jdt.path" : "$[?(@.RemoveThis == true)]" - }, - "C" : { - //Specify a relative path to the node - "@jdt.remove" : { - "@jdt.path" : "@.C2.C21" - } - } -} -``` - -Result: -``` javascript -{ - "B" : { - "RemoveThis" : false - }, - "C" : { - "C1" : 1, - "C2" : { - } - } -} -``` - -### Merge - -| Use: | `"@jdt.merge" : ` (Case sensitive) | -| ---- |:-----------------------------------------:| - - -| Value Type: | Behavior | -| ----------- | ----------------------------------------------------------- | -| Primitive | Replaces the value of the current node with the given value | -| Object | Recursively merges the object into the current node. Keys that are not present in the source file will be added.
If the object contains JDT attributes, apply them. See Attributes. -| Array | Applies merge with each element of the array as the transformation value.
In an explicit merge, if the transformation value should be the array, double brackets should be used (e.g. `[[]]`). In a default transformation, this is not necessary. - -**Obs:** If the transformation value does not match the source value for an already existing node, the transformation value will replace the existing one. - -#### Path Attribute - -The `@jdt.path` attribute can be used if a specific node or multiple nodes should be changed. It can also be used to change nodes within arrays. See Attributes for more information. - -Source: -``` javascript -{ - "A": { - "TransformThis": true - }, - "B": { - "TransformThis": false - }, - "C": { - }, - "D": { - "TransformThis": "WrongValue" - }, - "E": { - "TransformThis": false, - "Items": [ - { - "Value": 10 - }, - { - "Value": 20 - }, - { - "Value": 30 - } - ] - } -} -``` - -Transform: -``` javascript -{ - //Executes for all nodes on this level - "@jdt.merge" : [{ - "@jdt.path" : "$.*", - "@jdt.value" : { - "Default" : 0 - } - }, - //This only executes for matching nodes - { - "@jdt.path" : "$[?(@.TransformThis == true)]", - "@jdt.Value" : { - "Transformed" : true - } - }], - "E": { - // Accessing objects in array - "@jdt.merge" : { - "@jdt.path" : "$.Items[?(@.Value < 15)]", - "@jdt.value" : { - "Value" : 15, - "Changed" : true - } - } - } -} -``` - -Result: -``` javascript -{ - "A": { - "TransformThis" : true, - "Default" : 0, - "Transformed" : true - }, - "B": { - "TransformThis": false, - "Default" : 0 - }, - "C": { - "Default" : 0 - }, - "D": { - "TransformThis": "WrongValue", - "Default" : 0 - }, - "E": { - "TransformThis": false, - "Items": [ - { - "Value" : 15, - "Changed" : true - }, - { - "Value": 20 - }, - { - "Value": 30 - } - ], - "Default" : 0 - } -} -``` - -#### Value Attribute - -The `@jdt.value` attribute in a Merge is the only type that supports nested transformations. This means that transformations that should be executed in newly created or merged nodes can be added through this value. - -### Replace - -| Use: | `"@jdt.replace" : ` (Case sensitive) | -| ---- |:-------------------------------------------:| - - -| Value Type: | Behavior | -| ----------- | ----------------------------------------------------------- | -| Primitive | Replaces the current node with the given value | -| Object | If the object contains JDT attributes, apply them. See Attributes.
If not, replaces the node with the given object. -| Array | Applies merge with each element of the array as the transformation value.
If the transformation value is an array, replace the node with the given array. - -#### Example - -Source: -``` javascript -{ - "A" : { - "A1" : "11" - }, - "B" : { - "1B" : 12, - "2B" : 22 - }, - "C" : { - "C1" : 31, - "C2" : 32 - } -} -``` - -Transform: -``` javascript -{ - "A": { - "@jdt.replace": 1 - }, - "B": { - "@jdt.replace": { - "B1": 11, - "B2": 12 - } - }, - "C": { - // Double brackets are needed to specify - // the array as the transformation value - "@jdt.replace": [[ - { - "Value": 31 - }, - { - "Value": 32 - } - ]] - } -} -``` - -Result: -``` javascript -{ - "A" : 1, - "B" : { - "B1" : 11, - "B2" : 12 - }, - "C" : [ - { - "Value": 31 - }, - { - "Value": 32 - } - ] -} -``` - -#### Path Attribute - -Source: -``` javascript -{ - "A" : { - "A1" : 11, - "A2" : "Replace" - }, - "B" : [ - { - "ReplaceThis" :true - }, - { - "ReplaceThis" : false - } - ] -} -``` - -Transform: -``` javascript -{ - "@jdt.replace" : { - "@jdt.path" : "$.A.A2", - "@jdt.value" : 12 - }, - "B" : { - "@jdt.replace" : { - "@jdt.path" : "@[?(@.ReplaceThis == true)]", - "@jdt.value" : { - "Replaced" : true - } - } - } -} -``` - -Result: -``` javascript -{ - "A" : { - "A1" : 11, - "A2" : 12 - }, - "B" : [ - { - // The entire object was replaced - "Replaced" : true - }, - { - "ReplaceThis" : false - } - ] -} -``` - -## Order of Execution - -Transformations should be done in depth-first order to guarantee that everything is executed. Breadth-first order would have a slightly faster execution time as Remove transformations would potentially exclude other transformations from child nodes. For the same reason, this ordering could exclude transformations on lower level nodes that a user would like to execute. - -In the same level, the order of priority for transformation is as follows: - -Remove > Replace > Merge > Default > Rename - -This order guarantees that explicitly named transforms execute first. It also guarantees that removals prevent unnecessary transformations from occurring. - -### Processing Transform Files - -To guarantee the order of execution defined previously, the transformed file is processed in the following order. - -Starting from the root node: - -1) Enqueue all JDT transformations, identified by @jdt verbs -2) Iterate through the rest of the node - 1. Object - 1. If it exists in the original file: step into the object and start from step 1 - 2. If it does not exist, enqueue a merge transformation - 2. Array: Enqueue a merge transformation - 3. Primitive: Enqueue a merge (replace) transformation -3) Process all the transformation in the queue, per the order of execution From 9dc1d3edf8b4402dd881e1f1ac9e72200ae1951c Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 16:51:41 -0700 Subject: [PATCH 6/7] Readding logging entry point --- .../JsonTransformationTestLogger.cs | 6 ++++++ .../IJsonTransformationLogger.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs index 50d0c017..325d1080 100644 --- a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs @@ -56,6 +56,12 @@ public void LogMessage(string message) this.MessageLog.Add(new TestLogEntry(message, null, 0, 0, false)); } + /// + public void LogMessage(string message, string fileName, int lineNumber, int linePosition) + { + this.MessageLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); + } + /// public void LogWarning(string message) { diff --git a/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs b/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs index 968f053b..bfcacd28 100644 --- a/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs +++ b/src/Microsoft.VisualStudio.Jdt/IJsonTransformationLogger.cs @@ -16,6 +16,15 @@ public interface IJsonTransformationLogger /// The message text void LogMessage(string message); + /// + /// Logs a message + /// + /// The message + /// The full path to the file that caused the message. Can be null + /// The line that caused the message + /// The position in the line that caused the message + void LogMessage(string message, string fileName, int lineNumber, int linePosition); + /// /// Logs a warning /// From 9b0416590a954a5d01f3f57487cfce6577dccd64 Mon Sep 17 00:00:00 2001 From: Davi Paulino Date: Mon, 8 May 2017 17:04:41 -0700 Subject: [PATCH 7/7] Chaning struct fields to properties --- .../JsonTransformationTestLogger.cs | 106 ++++++++++++------ 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs index 325d1080..466fb8bd 100644 --- a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs @@ -29,55 +29,106 @@ public class JsonTransformationTestLogger : IJsonTransformationLogger /// public void LogError(string message) { - this.ErrorLog.Add(new TestLogEntry(message, null, 0, 0, false)); + this.ErrorLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); } /// public void LogError(string message, string fileName, int lineNumber, int linePosition) { - this.ErrorLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); + this.ErrorLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); } /// public void LogErrorFromException(Exception ex) { - this.ErrorLog.Add(new TestLogEntry(ex.Message, null, 0, 0, true)); + this.ErrorLog.Add(new TestLogEntry() + { + Message = ex.Message, + FromException = false + }); } /// public void LogErrorFromException(Exception ex, string fileName, int lineNumber, int linePosition) { - this.ErrorLog.Add(new TestLogEntry(ex.Message, fileName, lineNumber, linePosition, true)); + this.ErrorLog.Add(new TestLogEntry() + { + Message = ex.Message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = true + }); } /// public void LogMessage(string message) { - this.MessageLog.Add(new TestLogEntry(message, null, 0, 0, false)); + this.MessageLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); } /// public void LogMessage(string message, string fileName, int lineNumber, int linePosition) { - this.MessageLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); + this.MessageLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); } /// public void LogWarning(string message) { - this.WarningLog.Add(new TestLogEntry(message, null, 0, 0, false)); + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); } /// public void LogWarning(string message, string fileName) { - this.WarningLog.Add(new TestLogEntry(message, fileName, 0, 0, false)); + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = 0, + LinePosition = 0, + FromException = false + }); } /// public void LogWarning(string message, string fileName, int lineNumber, int linePosition) { - this.WarningLog.Add(new TestLogEntry(message, fileName, lineNumber, linePosition, false)); + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); } /// @@ -87,46 +138,29 @@ public void LogWarning(string message, string fileName, int lineNumber, int line public struct TestLogEntry { /// - /// The log message - /// - public string Message; - - /// - /// The file that caused the entry + /// Gets or sets the log message /// - public string FileName; + public string Message { get; set; } /// - /// The line in the file + /// Gets or sets the file that caused the entry /// - public int LineNumber; + public string FileName { get; set; } /// - /// The position in the line + /// Gets or sets the line in the file /// - public int LinePosition; + public int LineNumber { get; set; } /// - /// Whether the entry was caused from an exception + /// Gets or sets the position in the line /// - public bool FromException; + public int LinePosition { get; set; } /// - /// Initializes a new instance of the struct. + /// Gets or sets a value indicating whether whether the entry was caused from an exception /// - /// The entry message - /// The file that caused the entry - /// The line in the file - /// The position in the line - /// Whether the entry was caused by an exception - public TestLogEntry(string message, string file, int lineNumber, int linePosition, bool fromException) - { - this.Message = message; - this.FileName = file; - this.LineNumber = lineNumber; - this.LinePosition = linePosition; - this.FromException = fromException; - } + public bool FromException { get; set; } } } }