diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index c265099..1d8efec 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -253,6 +253,69 @@ public static class ForgeSchemaHelper } "; + public const string ChildSelectorLiteralChild = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""ChildSelector"": [ + { + ""Label"": ""Literal child"", + ""Child"": ""LiteralLeaf"" + } + ] + }, + ""LiteralLeaf"": { + ""Type"": ""Leaf"" + } + } + } + "; + + public const string ChildSelectorDynamicChild = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_TardigradeAction"": { + ""Action"": ""TardigradeAction"" + } + }, + ""ChildSelector"": [ + { + ""Label"": ""Dynamic child"", + ""ShouldSelect"": ""C#|Session.GetLastActionResponse().Status == \""Success\"""", + ""Child"": ""C#|UserContext.ResourceType == \""Container\"" ? \""DynamicLeaf\"" : \""UnexpectedLeaf\"""" + } + ] + }, + ""DynamicLeaf"": { + ""Type"": ""Leaf"" + }, + ""UnexpectedLeaf"": { + ""Type"": ""Leaf"" + } + } + } + "; + + public const string ChildSelectorDynamicChildInvalidType = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""ChildSelector"": [ + { + ""Label"": ""Invalid dynamic child"", + ""Child"": ""C#|1"" + } + ] + } + } + } + "; + public const string TestEvaluateInputType_FailOnField_Action = @" { ""Tree"": { @@ -571,4 +634,4 @@ public static class ForgeSchemaHelper } "; } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs index 8dbc702..97504ef 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -531,6 +531,41 @@ public void TestTreeWalkerSession_WalkTree_NoChildMatched() Assert.AreEqual("RanToCompletion_NoChildMatched", actualStatus, "Expected WalkTree to end with NoChildMatched status."); } + [TestMethod] + public void TestTreeWalkerSession_WalkTree_ChildSelectorLiteralChild_Success() + { + this.TestInitialize(jsonSchema: ForgeSchemaHelper.ChildSelectorLiteralChild); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion with a literal child selector."); + Assert.AreEqual("LiteralLeaf", this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected the literal child selector to visit LiteralLeaf."); + } + + [TestMethod] + public void TestTreeWalkerSession_WalkTree_ChildSelectorDynamicChild_Success() + { + this.TestInitialize(jsonSchema: ForgeSchemaHelper.ChildSelectorDynamicChild); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus, "Expected WalkTree to run to completion with a dynamic child selector."); + Assert.AreEqual("DynamicLeaf", this.session.GetCurrentTreeNode().GetAwaiter().GetResult(), "Expected the dynamic child selector to resolve to DynamicLeaf."); + } + + [TestMethod] + public void TestTreeWalkerSession_WalkTree_ChildSelectorDynamicChildInvalidType_Failure() + { + this.TestInitialize(jsonSchema: ForgeSchemaHelper.ChildSelectorDynamicChildInvalidType); + + string actual; + Assert.ThrowsException(() => + { + actual = this.session.WalkTree("Root").GetAwaiter().GetResult(); + }, "Expected WalkTree to fail because the dynamic child selector did not evaluate to a string child key."); + + actual = this.session.Status; + Assert.AreEqual("Failed_EvaluateDynamicProperty", actual, "Expected WalkTree to fail with Failed_EvaluateDynamicProperty when ChildSelector.Child is an invalid dynamic type."); + } + [TestMethod] public void TestReexecutingNode_WithoutRetryCurrentTreeNodeActionsFlag_Success() { @@ -1347,4 +1382,4 @@ private TreeWalkerSession InitializeSubroutineTree(SubroutineInput subroutineInp return new TreeWalkerSession(subroutineParameters); } } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/Forge.TreeWalker.csproj b/Forge.TreeWalker/Forge.TreeWalker.csproj index 22cbab8..f0904c0 100644 --- a/Forge.TreeWalker/Forge.TreeWalker.csproj +++ b/Forge.TreeWalker/Forge.TreeWalker.csproj @@ -39,4 +39,4 @@ true - \ No newline at end of file + diff --git a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json index 3af7616..e97e545 100644 --- a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json +++ b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json @@ -171,7 +171,8 @@ "type": "string" }, "Child": { - "type": "string" + "type": "string", + "description": "Literal child TreeNode key or a dynamic expression string (for example, C#|...) that resolves to a child TreeNode key." } }, "additionalProperties": false, @@ -281,4 +282,4 @@ ] } } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index 38d9834..09285e2 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -101,6 +101,8 @@ public class ChildSelector /// /// String key pointer to a child TreeNode. + /// This may be either a literal child TreeNode key or a string starting with + /// that evaluates to a child TreeNode key. /// Visit this child if the attached ShouldSelect expression evaluates to true. /// [DataMember(IsRequired = true)] @@ -277,4 +279,4 @@ public enum TreeNodeType [EnumMember] Subroutine = 4 } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 38efbf3..f15b5a7 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -536,14 +536,14 @@ internal async Task SelectChild(TreeNode treeNode) // Empty expressions default to true. Otherwise, evaluate the expression. if (string.IsNullOrWhiteSpace(cs.ShouldSelect) && !string.IsNullOrWhiteSpace(cs.Child)) { - return cs.Child; + return (string)await this.EvaluateDynamicProperty(cs.Child, typeof(string)).ConfigureAwait(false); } var evaluationResult = await this.EvaluateDynamicProperty(cs.ShouldSelect, typeof(bool)).ConfigureAwait(false); try { if ((bool)evaluationResult) { - return cs.Child; + return (string)await this.EvaluateDynamicProperty(cs.Child, typeof(string)).ConfigureAwait(false); } } catch (Exception e) @@ -1260,4 +1260,4 @@ public static void GetActionsMapFromAssembly(Assembly forgeActionsAssembly, out } } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index a69d063..4e22357 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Key Differentiators: Forge has 3 major components: ForgeTree, TreeWalker, and ForgeEditor. * **ForgeTree** is the JSON data contract that defines the tree structure. It contains normal tree-concept objects such as TreeNodes and ChildSelectors, as well as TreeActions and other properties. - _In this example, the Container_TreeNode is an Action type node. It executes a CollectDiagnosticsAction with an Input object containing a Command property. The value of Command is set through a Roslyn expression that calls into the application to get the UserContext.GetCommand() result. In the child selector we see another Roslyn expression that gets the persisted action response, and visits the Tardigrade_TreeNode if the Status is successful._ + _In this example, the Container_TreeNode is an Action type node. It executes a CollectDiagnosticsAction with an Input object containing a Command property. The value of Command is set through a Roslyn expression that calls into the application to get the UserContext.GetCommand() result. In the child selector we see a Roslyn expression that gets the persisted action response, and the Child value is also resolved dynamically from a Roslyn expression that returns the next child TreeNode key._ ```yaml "Container_TreeNode": { @@ -53,12 +53,14 @@ Forge has 3 major components: ForgeTree, TreeWalker, and ForgeEditor. "ChildSelector": [ { "ShouldSelect": "C#|Session.GetLastActionResponse().Status == \"Success\"", - "Child": "Tardigrade_TreeNode" + "Child": "C#|UserContext.ResourceType == \"Container\" ? \"Tardigrade_TreeNode\" : \"ContainerFallback_TreeNode\"" } ] } ``` +`ChildSelector.Child` can be either a literal child node key or a `C#|...` expression that evaluates to one. + * **TreeWalker** takes in the ForgeTree and other parameters, and walks the tree to completion. It calls application-defined callbacks and actions, passing in dynamically evaluated properties from the ForgeTree. The TreeWalker makes decisions at runtime about the path it walks by utilizing Roslyn to evaluate C# code-snippets from the ForgeTree. * **ForgeEditor** Web version is available at https://forge-editor.microsoft.com/ or https://aka.ms/forge-editor. ForgeEditor is an Electron application that allows you to visualize and edit the ForgeTree in a clean UI experience. It contains features such as: tree visualization, buttons to create/delete TreeNodes, auto-complete when editing JSON file, text highlighting when hovering over TreeNode, etc..