diff --git a/src/AssemblySharedInfoGenerator/AssemblySharedInfo.cs b/src/AssemblySharedInfoGenerator/AssemblySharedInfo.cs index fc7e696bd65..1a847fc937c 100644 --- a/src/AssemblySharedInfoGenerator/AssemblySharedInfo.cs +++ b/src/AssemblySharedInfoGenerator/AssemblySharedInfo.cs @@ -45,7 +45,7 @@ // to distinguish one build from another. AssemblyFileVersion is specified // in AssemblyVersionInfo.cs so that it can be easily incremented by the // automated build process. -[assembly: AssemblyVersion("2.1.0.4395")] +[assembly: AssemblyVersion("2.1.0.5656")] // By default, the "Product version" shown in the file properties window is @@ -64,4 +64,4 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("2.1.0.4395")] +[assembly: AssemblyFileVersion("2.1.0.5656")] diff --git a/src/Dynamo.All.sln b/src/Dynamo.All.sln index cb8f2c941a9..5642cdb82ea 100644 --- a/src/Dynamo.All.sln +++ b/src/Dynamo.All.sln @@ -223,6 +223,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoPackagesWPF", "Dynamo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoWPFCLI", "DynamoWPFCLI\DynamoWPFCLI.csproj", "{F7FD9395-35D5-474D-8AD1-9904817E70D3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackingNodeModels", "Libraries\PackingNodeModels\PackingNodeModels.csproj", "{D915633B-8BD6-4860-B0A7-4D689DF445FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackingNodeTests", "..\test\Libraries\PackingNodeTests\PackingNodeTests.csproj", "{79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -513,6 +517,14 @@ Global {F7FD9395-35D5-474D-8AD1-9904817E70D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7FD9395-35D5-474D-8AD1-9904817E70D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7FD9395-35D5-474D-8AD1-9904817E70D3}.Release|Any CPU.Build.0 = Release|Any CPU + {D915633B-8BD6-4860-B0A7-4D689DF445FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D915633B-8BD6-4860-B0A7-4D689DF445FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D915633B-8BD6-4860-B0A7-4D689DF445FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D915633B-8BD6-4860-B0A7-4D689DF445FA}.Release|Any CPU.Build.0 = Release|Any CPU + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -579,6 +591,8 @@ Global {F8D2E3E5-4505-4463-9367-2EB2629D7DCD} = {0E492D35-2310-4849-9694-A2A53C09F21B} {AE7F2579-104A-4AF4-AA7B-614AE9E79279} = {24A1EF6B-5A24-4536-B8BD-C879ABA26389} {C0D6DEE5-5532-4345-9C66-4C00D7FDB8BE} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13} + {D915633B-8BD6-4860-B0A7-4D689DF445FA} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13} + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF} = {0E492D35-2310-4849-9694-A2A53C09F21B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {89CB19C6-BF0A-4E6A-BFDA-79D143EAB59D} diff --git a/src/DynamoCore/Graph/Nodes/NodeCategories.cs b/src/DynamoCore/Graph/Nodes/NodeCategories.cs index 3593c7738b9..996c4014ece 100644 --- a/src/DynamoCore/Graph/Nodes/NodeCategories.cs +++ b/src/DynamoCore/Graph/Nodes/NodeCategories.cs @@ -30,6 +30,7 @@ public static class BuiltinNodeCategories public const string CORE_SCRIPTING = "Core.Scripting"; public const string CORE_IO = "Core.File"; public const string CORE_UNITS = "Core.Units"; + public const string CORE_PACKING = "Core.Packing"; public const string LOGIC = "Core.Logic"; public const string LOGIC_MATH_OPTIMIZE = "Logic.Math.Optimize"; diff --git a/src/Libraries/CoreNodes/CoreNodes.csproj b/src/Libraries/CoreNodes/CoreNodes.csproj index 07f2f2385ec..78aaf2e32da 100644 --- a/src/Libraries/CoreNodes/CoreNodes.csproj +++ b/src/Libraries/CoreNodes/CoreNodes.csproj @@ -77,6 +77,7 @@ + True diff --git a/src/Libraries/CoreNodes/Packing.cs b/src/Libraries/CoreNodes/Packing.cs new file mode 100644 index 00000000000..47e01e9d89d --- /dev/null +++ b/src/Libraries/CoreNodes/Packing.cs @@ -0,0 +1,111 @@ +using Autodesk.DesignScript.Runtime; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DSCore +{ + [IsVisibleInDynamoLibrary(false)] + public class PackFunctions + { + /// + /// Packs data to a dictionary while manually handling lacing in a "Longest" strategy. + /// + /// + /// Ordered list of keys to use in the output dictionary + /// Ordered list of whether a value is a collection or not, which helps differenciating Lacing from a normal array value. + /// Ordered ArrayList or List of ArrayList defining the data to be packed. + /// List of Dictionary matching the ordered keys and data lists. + public static object PackOutputAsDictionary(List keys, List isCollection, [ArbitraryDimensionArrayImport] object data) + { + if (keys == null || keys.Count == 0 || isCollection == null || isCollection.Count == 0 || data == null) + { + return null; + } + + var result = new List>(); + + var inputs = data as ArrayList; + + var outputCount = GetOuputCountFrom(isCollection, inputs); + + for (int outputIndex = 0; outputIndex < outputCount; ++outputIndex) + { + result.Add(CreateOuputDictionary(keys, isCollection, inputs, outputIndex)); + } + + return result; + } + + private static Dictionary CreateOuputDictionary(List keys, List isCollection, ArrayList inputs, int outputIndex) + { + var dictionary = new Dictionary(); + + for (int inputIndex = 0; inputIndex < inputs.Count; ++inputIndex) + { + object value = inputs[inputIndex]; + + if (value is ArrayList) + { + var array = value as ArrayList; + if (!isCollection[inputIndex] || array[0] is ArrayList) + { + int lastIndex = array.Count - 1; + value = lastIndex < outputIndex ? array[lastIndex] : array[outputIndex]; + } + } + + dictionary[keys[inputIndex]] = value; + } + + return dictionary; + } + + /// + /// Returns the number of dictionaries that should be output by the node. + /// LacingStrategy "Longest" is used, which means the biggest list is the count of dictionaries that should be output. + /// + /// + /// + /// + private static int GetOuputCountFrom(List isCollection, ArrayList inputs) + { + var subArrays = inputs.Cast() + .Where(i => i is ArrayList) + .Cast(); + + if (!subArrays.Any()) return 1; + + return subArrays.Max(subArray => + { + var index = inputs.IndexOf(subArray); + if (!isCollection[index] || (subArray[0] is ArrayList)) + { + return subArray.Count; + } + + return 1; + }); + } + } + + [IsVisibleInDynamoLibrary(false)] + public class UnPackFunctions + { + /// + /// Returns the value of dictionary at a given key. + /// + /// Dictionary exposing all available values + /// The key of the value to retrieve + /// The value retrieved from the dictionary at the given key + public static object UnPackOutputByKey(DesignScript.Builtin.Dictionary dictionary, string key) + { + if (dictionary == null) return null; + + return dictionary.ValueAtKey(key); + } + } +} diff --git a/src/Libraries/PackingNodeModels/Pack/Pack.cs b/src/Libraries/PackingNodeModels/Pack/Pack.cs new file mode 100644 index 00000000000..799f6e49718 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Pack/Pack.cs @@ -0,0 +1,112 @@ +using Dynamo.Graph.Nodes; +using Newtonsoft.Json; +using PackingNodeModels.Pack.Validation; +using PackingNodeModels.Properties; +using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PackingNodeModels.Pack +{ + /// + /// Creates a node that takes in a TypeDefinition and input values defined by the given TypeDefinition and outputs it as dictionary. + /// + [NodeName("Pack")] + [NodeCategory(BuiltinNodeCategories.CORE_PACKING)] + [NodeDescription("PackNodeDescription", typeof(Resource))] + [OutPortNames("Out")] + [OutPortTypes("Dictionary")] + [OutPortDescriptions("Dictionary")] + [IsDesignScriptCompatible] + public class Pack : PackingNode + { + private List cachedValues; + private IValidationManager validationManager; + + public Pack() + { + validationManager = new ValidationManager(this); + } + + /// + /// Private constructor used for serialization. + /// + /// A collection of objects. + /// A collection of objects. + [JsonConstructor] + protected Pack(IEnumerable inPorts, IEnumerable outPorts) + : base(inPorts, outPorts) + { + validationManager = new ValidationManager(this); + } + + protected override void RefreshTypeDefinitionPorts() + { + cachedValues = null; + InPorts.Skip(1).ToList().ForEach(port => InPorts.Remove(port)); + + if (TypeDefinition != null) + { + foreach (var property in TypeDefinition.Properties) + { + InPorts.Add(new PortModel(PortType.Input, this, new PortData(property.Key, property.Value.ToString()))); + } + } + } + + protected override void ValidateInputs(List values) + { + var wasInWarningState = validationManager.Warnings.Any(); + if (values.Count > 1) + { + var valuesByIndex = new Dictionary(); + for (int i = 1; i < values.Count; ++i) + { + if (cachedValues == null || cachedValues.Count <= i || values[i] != cachedValues[i]) + { + valuesByIndex[i] = values[i]; + } + } + + validationManager.HandleValidation(valuesByIndex); + } + + cachedValues = values; + + //FIXME This doesn't seem right. Is there a way to build a conditional node that would depend on validation inputs, instead of building the output twice? + //Right now, it's ran once, then the DataBridge is invoked, which runs the validation and re-trigger the building of the ouput if warnings are gone/new. + if (wasInWarningState != validationManager.Warnings.Any()) + { + OnNodeModified(true); + } + } + + public override IEnumerable BuildOutputAst(List inputAstNodes) + { + var baseOutput = base.BuildOutputAst(inputAstNodes).ToList(); + + if (inputAstNodes == null || !IsValidInputState(inputAstNodes) || TypeDefinition == null) + { + baseOutput.Add(AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode())); + return baseOutput; + } + + inputAstNodes = inputAstNodes.Skip(1).ToList(); + inputAstNodes.Add(AstFactory.BuildStringNode(TypeDefinition.Name)); + var keys = TypeDefinition.Properties.Select(input => AstFactory.BuildStringNode(input.Key) as AssociativeNode).ToList(); + keys.Add(AstFactory.BuildStringNode("typeid")); + //FIXME Only way I found to pass in the context to the output function call so it knows how replications should work. + //Maybe a tuple of keys,context would be better? + var isCollectionInputs = TypeDefinition.Properties.Select(input => AstFactory.BuildBooleanNode(input.Value.IsCollection) as AssociativeNode).ToList(); + + var functionCall = AstFactory.BuildFunctionCall( + new Func, List, object, object>(DSCore.PackFunctions.PackOutputAsDictionary), + new List { AstFactory.BuildExprList(keys), AstFactory.BuildExprList(isCollectionInputs), AstFactory.BuildExprList(inputAstNodes) }); + + baseOutput.Add(AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall)); + + return baseOutput; + } + } +} diff --git a/src/Libraries/PackingNodeModels/Pack/Validation/PortValidator.cs b/src/Libraries/PackingNodeModels/Pack/Validation/PortValidator.cs new file mode 100644 index 00000000000..2252e1441f5 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Pack/Validation/PortValidator.cs @@ -0,0 +1,195 @@ +using Dynamo.Graph.Nodes; +using PackingNodeModels.Properties; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PackingNodeModels.Pack.Validation +{ + /// + /// Used to validate that a given value matches the type defined by the PortModel+PropertyType combination of a Pack node InPort. + /// Comes with a basic mapping of known types to Dynamo Types. Assumes that an unknown type is coming from another Pack node. + /// + internal class PortValidator + { + /// + /// "Primitive" known to Pack node. + /// + private static Dictionary> CompatibleTypes = new Dictionary> + { + { "Bool", new List() { typeof(bool), typeof(Boolean) } }, + { "Int32", new List() { typeof(int), typeof(Int32) } }, + { "String", new List() { typeof(string) } }, + { "Float64", new List() { typeof(float), typeof(double), typeof(Int32), typeof(Int64) } }, + { "Arc", new List() { typeof(Autodesk.DesignScript.Geometry.Arc) } }, + { "Circle", new List() { typeof(Autodesk.DesignScript.Geometry.Circle) } }, + { "CoordinateSystem", new List() { typeof(Autodesk.DesignScript.Geometry.CoordinateSystem) } }, + { "Cone", new List() { typeof(Autodesk.DesignScript.Geometry.Cone) } }, + { "Curve", new List() { typeof(Autodesk.DesignScript.Geometry.Curve) } }, + { "Cuboid", new List() { typeof(Autodesk.DesignScript.Geometry.Cuboid) } }, + { "Cylinder", new List() { typeof(Autodesk.DesignScript.Geometry.Cylinder) } }, + { "Ellipse", new List() { typeof(Autodesk.DesignScript.Geometry.Ellipse) } }, + { "Line", new List() { typeof(Autodesk.DesignScript.Geometry.Line) } }, + { "Plane", new List() { typeof(Autodesk.DesignScript.Geometry.Plane) } }, + { "Point", new List() { typeof(Autodesk.DesignScript.Geometry.Point) } }, + { "PolyCurve", new List() { typeof(Autodesk.DesignScript.Geometry.PolyCurve) } }, + { "Polygon", new List() { typeof(Autodesk.DesignScript.Geometry.Polygon) } }, + { "Rectangle", new List() { typeof(Autodesk.DesignScript.Geometry.Rectangle) } }, + { "Sphere", new List() { typeof(Autodesk.DesignScript.Geometry.Sphere) } }, + { "Vector", new List() { typeof(Autodesk.DesignScript.Geometry.Vector) } }, + { "Surface", new List() { typeof(Autodesk.DesignScript.Geometry.Surface) } }, + { "Geometry", new List() { typeof(Autodesk.DesignScript.Geometry.Geometry) } } //TODO SolidDef or Geometry? + }; + + /// + /// Validates a value against its port's associated property. + /// + /// + /// + /// + /// + public static string Validate(KeyValuePair property, object value, PortModel portModel) + { + if (value == null) + { + return string.Format(Resource.PortValidatorNullValueMessage, property.Key, property.Value.Type); + } + + if (property.Value.IsCollection) + { + return ValidateCollection(property, value, portModel); + } + + return ValidateSingleValue(property, value, portModel); + } + + private static string ValidateSingleValue(KeyValuePair property, object value, PortModel portModel) + { + if (value is ArrayList arrayList) + { + return ValidateSingleValueLacing(property, arrayList, portModel); + } + + return ValidateTypeMatch(property, value, portModel); + } + + private static string ValidateSingleValueLacing(KeyValuePair property, ArrayList values, PortModel portModel) + { + if (ContainsCollections(values)) + { + return string.Format(Resource.PortValidatorArrayInsteadOfSingleValueMessage, property.Key, property.Value.Type); + } + + return ValidateValues(property, values, portModel); + } + + private static string ValidateCollection(KeyValuePair property, object value, PortModel portModel) + { + if (!(value is ArrayList)) + { + return string.Format(Resource.PortValidatorSingleValueInsteadOfArrayMessage, property.Key, property.Value.Type); + } + + var values = value as ArrayList; + + if (IsMixedCombinationOfCollectionAndSingleValues(values)) + { + return string.Format(Resource.PortValidatorMixedCombinationMessage, property.Key, property.Value.Type); + } + + if (ContainsCollections(values)) + { + return ValidateArrayLacing(property, values, portModel); + } + + return ValidateValues(property, values, portModel); + } + + private static string ValidateArrayLacing(KeyValuePair property, ArrayList values, PortModel portModel) + { + foreach (var subArray in values) + { + foreach (var subValue in subArray as ArrayList) + { + //Deeper arrays not allowed + if (subValue is ArrayList) + { + return string.Format(Resource.PortValidatorNestedArrayMessage, property.Key, property.Value.Type); + } + else + { + var validation = ValidateTypeMatch(property, subValue, portModel); + if (!string.IsNullOrEmpty(validation)) return validation; + } + + } + } + + return null; + } + + public static string ValidateTypeMatch(KeyValuePair property, object value, PortModel portModel) + { + if (IsKnownType(property.Value.Type)) + { + if (!IsTypeMatch(value, property.Value.Type)) + { + return string.Format(Resource.PortValidatorWrongTypeMessage, property.Key, property.Value.Type, value?.GetType()); + } + + return null; + } + + return ValidateUnknownType(property, value, portModel); + } + + private static string ValidateValues(KeyValuePair property, ArrayList values, PortModel portModel) + { + foreach (var value in values) + { + var validation = ValidateTypeMatch(property, value, portModel); + + if (!string.IsNullOrEmpty(validation)) return validation; + } + + return null; + } + + private static string ValidateUnknownType(KeyValuePair property, object value, PortModel portModel) + { + //Assume and expect that an unkwown type comes from another Pack + var owner = portModel.Connectors[0].Start.Owner as Pack; + if (owner == null || !property.Value.Type.Equals(owner.TypeDefinition.Name, StringComparison.InvariantCultureIgnoreCase)) + { + return string.Format(Resource.PortValidatorWrongTypeMessage, property.Key, property.Value.Type, owner?.TypeDefinition.Name ?? value?.GetType().ToString()); + } + + return null; + } + + private static bool ContainsCollections(ArrayList values) + { + return values.Cast().Any(x => x is ArrayList); + } + + private static bool IsMixedCombinationOfCollectionAndSingleValues(ArrayList values) + { + return values.Cast().Select(x => x is ArrayList).Distinct().Count() > 1; + } + + private static bool IsKnownType(string type) + { + return CompatibleTypes.ContainsKey(type); + } + + private static bool IsTypeMatch(object value, string expectedType) + { + if (!CompatibleTypes.ContainsKey(expectedType)) return true; + + return CompatibleTypes[expectedType].Exists(x => x == value?.GetType()); + } + } +} diff --git a/src/Libraries/PackingNodeModels/Pack/Validation/ValidationManager.cs b/src/Libraries/PackingNodeModels/Pack/Validation/ValidationManager.cs new file mode 100644 index 00000000000..a009a7dbacd --- /dev/null +++ b/src/Libraries/PackingNodeModels/Pack/Validation/ValidationManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PackingNodeModels.Pack.Validation +{ + internal interface IValidationManager + { + void HandleValidation(Dictionary valuesByIndex); + ReadOnlyCollection Warnings { get; } + } + + /// + /// Manages validation warnings for a given Pack node. + /// Warnings are added to the Pack node and kept track of in the manager. + /// + internal class ValidationManager : IValidationManager + { + private List warnings; + + public ReadOnlyCollection Warnings { get { return warnings.AsReadOnly(); } } + + public Pack Node { get; private set; } + + public ValidationManager(Pack node) + { + Node = node; + warnings = new List(); + } + + /// + /// Validates values from the Pack node and add and/or remove warnings according to the validation result. + /// + /// + public void HandleValidation(Dictionary valuesByIndex) + { + var properties = Node.TypeDefinition.Properties.ToList(); + foreach (var pair in valuesByIndex) + { + ClearWarningForPortIndex(pair.Key); + + if (!Node.InPorts[pair.Key].Connectors.Any()) continue; + + var validation = PortValidator.Validate(properties[pair.Key - 1], pair.Value, Node.InPorts[pair.Key]); //i - 1 because we're skipping the Type port. + + if (!string.IsNullOrEmpty(validation)) + { + warnings.Add(new Validation { Message = validation, PortIndex = pair.Key }); + } + } + + ComputeWarnings(); + } + + private void ClearWarningForPortIndex(int index) + { + warnings.RemoveAll(w => w.PortIndex == index); + } + + private void ComputeWarnings() + { + Node.ClearErrorsAndWarnings(); + if (warnings.Any()) + { + Node.Warning(String.Join(String.Empty, warnings.Select(w => w.Message).ToArray()), true); + } + } + } + + internal class Validation + { + public string Message { get; set; } + public int PortIndex { get; set; } + } +} diff --git a/src/Libraries/PackingNodeModels/PackingNode.cs b/src/Libraries/PackingNodeModels/PackingNode.cs new file mode 100644 index 00000000000..a972a0f4c9a --- /dev/null +++ b/src/Libraries/PackingNodeModels/PackingNode.cs @@ -0,0 +1,206 @@ +using Dynamo.Controls; +using Dynamo.Graph.Nodes; +using Dynamo.Models; +using Dynamo.Scheduler; +using Dynamo.ViewModels; +using Dynamo.Wpf; +using Newtonsoft.Json; +using PackingNodeModels.Properties; +using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Threading; +using VMDataBridge; + +namespace PackingNodeModels +{ + /// + /// Base class for nodes that utilize the dynamic-like functionality of taking in a TypeDefinition as an input and redefining InPorts or OutPorts according to this definition. + /// Exposes two methods that should be overriden by extending classes : + /// -ValidateInputs - which will receive InPorts' data. + /// -RefreshTypeDefinitionPorts - which will be called after the TypeDefinition changes. + /// + public abstract class PackingNode : NodeModel + { + protected string TypeDefinitionPortName = Resource.TypePortName; + + /// + /// Event made to communicate with the NodeViewCustomization and request a scheduled action. + /// + public event Action RequestScheduledTask; + + private string _cachedTypeDefinition; + private TypeDefinition _typeDefinition; + + /// + /// A TypeDefinition that will be used by extending classes to define OutPorts and InPorts + /// Modifying the TypeDefinition implies clearing errors and warnings and calling RefreshTypeDefinitionPorts + /// + [JsonProperty("TypeDefinition")] + public TypeDefinition TypeDefinition + { + get + { + return _typeDefinition; + } + protected set + { + if (value == null) + { + _cachedTypeDefinition = null; + } + + _typeDefinition = value; + ClearErrorsAndWarnings(); + RequestScheduledTask?.Invoke(RefreshTypeDefinitionPorts); + } + } + + /// + /// Returns whether the node is in a valid state, that is if it does not have errors or warnings. + /// + [JsonIgnore] + public bool IsInValidState + { + get + { + return !IsInErrorState && State != ElementState.Warning && State != ElementState.PersistentWarning; + } + } + + /// + /// Base constructor used to define the constant Type InPort. + /// + public PackingNode() + { + InPorts.Add(new PortModel(PortType.Input, this, new PortData(TypeDefinitionPortName, Resource.TypePortTooltip))); + + RegisterAllPorts(); + } + + /// + /// Private constructor used for serialization. + /// + /// A collection of objects. + /// A collection of objects. + protected PackingNode(IEnumerable inPorts, IEnumerable outPorts) + : base(inPorts, outPorts) { } + + protected override void OnBuilt() + { + base.OnBuilt(); + DataBridge.Instance.RegisterCallback(GUID.ToString(), OnEvaluationComplete); + } + + public override IEnumerable BuildOutputAst(List inputAstNodes) + { + return new List() + { + AstFactory.BuildAssignment( + AstFactory.BuildIdentifier(AstIdentifierBase + "_dummy"), + DataBridge.GenerateBridgeDataAst(GUID.ToString(), AstFactory.BuildExprList(inputAstNodes ?? new List()))) + }; + } + + protected virtual void ValidateInputs(List values) { } + + protected abstract void RefreshTypeDefinitionPorts(); + + private void OnEvaluationComplete(object obj) + { + if (obj is ArrayList inputValues) + { + CheckTypeDefinition(inputValues); + + if (TypeDefinition != null) + { + ValidateInputs(inputValues.Cast().ToList()); + } + } + } + + private void CheckTypeDefinition(ArrayList inputValues) + { + if (InputNodes.Count == 0 || !InputNodes.ContainsKey(0) || InputNodes[0] == null) + { + TypeDefinition = null; + } + else + { + if (inputValues[0] is string typeDef) + { + if (typeDef != _cachedTypeDefinition) + { + try + { + TypeDefinition = TypeDefinitionParser.ParseType(typeDef); + _cachedTypeDefinition = typeDef; + } + catch (Sprache.ParseException e) + { + TypeDefinition = null; + Warning(e.Message, true); + } + } + } + else + { + Warning(Resource.TypePortWarning, true); + } + } + } + + protected bool IsValidInputState(List inputAstNodes) + { + return InPorts[0].Connectors.Any() && inputAstNodes.Count > 1 && IsInValidState && !inputAstNodes.Exists(node => node is NullNode); + } + + public override void Dispose() + { + base.Dispose(); + DataBridge.Instance.UnregisterCallback(GUID.ToString()); + } + } + + /// + /// ViewCustomization for PackingNodes whose purpose is being able to schedule actions from the PackingNode. + /// + public class PackingNodeView : INodeViewCustomization + { + private DynamoModel dynamoModel; + private DynamoViewModel dynamoViewModel; + private DispatcherSynchronizationContext syncContext; + private PackingNode node; + + public void CustomizeView(PackingNode nodeModel, NodeView nodeView) + { + dynamoModel = nodeView.ViewModel.DynamoViewModel.Model; + dynamoViewModel = nodeView.ViewModel.DynamoViewModel; + syncContext = new DispatcherSynchronizationContext(nodeView.Dispatcher); + node = nodeModel; + node.RequestScheduledTask += OnRequestScheduledTask; + } + + private void OnRequestScheduledTask(Action action) + { + var s = dynamoViewModel.Model.Scheduler; + + var t = new DelegateBasedAsyncTask(s, () => + { + }); + + t.ThenSend((_) => + { + action(); + }, syncContext); + + s.ScheduleForExecution(t); + } + + public void Dispose() + { + } + } +} diff --git a/src/Libraries/PackingNodeModels/PackingNodeModels.csproj b/src/Libraries/PackingNodeModels/PackingNodeModels.csproj new file mode 100644 index 00000000000..9bbea25907c --- /dev/null +++ b/src/Libraries/PackingNodeModels/PackingNodeModels.csproj @@ -0,0 +1,134 @@ + + + + + + + + Debug + AnyCPU + {D915633B-8BD6-4860-B0A7-4D689DF445FA} + Library + Properties + PackingNodeModels + PackingNodeModels + v4.5 + 512 + + + true + full + false + $(OutputPath)\nodes + DEBUG;TRACE + prompt + 4 + ..\..\..\bin\AnyCPU\Debug\en-US\PackingNodeModels.xml + + + pdbonly + true + $(OutputPath) + TRACE + prompt + 4 + ..\..\..\bin\AnyCPU\Release\en-US\PackingNodeModels.xml + + + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + False + + + + + ..\..\packages\Sprache.2.1.2\lib\net45\Sprache.dll + True + + + False + ..\..\..\extern\ProtoGeometry\ProtoGeometry.dll + False + + + + + + + + + + + + + + + + + + + True + True + Resource.resx + + + + + + + {51BB6014-43F7-4F31-B8D3-E3C37EBEDAF4} + DynamoCoreWpf + False + + + {7858FA8C-475F-4B8E-B468-1F8200778CF8} + DynamoCore + False + + + {7A9E0314-966F-4584-BAA3-7339CBB849D1} + ProtoCore + False + + + {ef879a10-041d-4c68-83e7-3192685f1bae} + DynamoServices + + + {87550b2b-6cb8-461e-8965-dfafe3aafb5c} + CoreNodes + False + + + {c0d6dee5-5532-4345-9c66-4c00d7fdb8be} + DesignScriptBuiltin + False + + + {CCB6E56B-2DA1-4EBA-A1F9-E8510E129D12} + VMDataBridge + False + + + + + + + + + ResXFileCodeGenerator + Resource.Designer.cs + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Libraries/PackingNodeModels/Properties/AssemblyInfo.cs b/src/Libraries/PackingNodeModels/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..7b829cee0b8 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PackingNodeModels")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PackingNodeModels")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d915633b-8bd6-4860-b0a7-4d689df445fa")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("PackingNodeTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Libraries/PackingNodeModels/Properties/Resource.Designer.cs b/src/Libraries/PackingNodeModels/Properties/Resource.Designer.cs new file mode 100644 index 00000000000..73d44878d12 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Properties/Resource.Designer.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PackingNodeModels.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PackingNodeModels.Properties.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Creates a dictionary given a TypeDefinition and its corresponding input values. + /// + internal static string PackNodeDescription { + get { + return ResourceManager.GetString("PackNodeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Out. + /// + internal static string PackNodeOutputPortName { + get { + return ResourceManager.GetString("PackNodeOutputPortName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected a single value of type {1} but received a list of values.. + /// + internal static string PortValidatorArrayInsteadOfSingleValueMessage { + get { + return ResourceManager.GetString("PortValidatorArrayInsteadOfSingleValueMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected an array of type {1} but received a mixed combination of single values and arrays.. + /// + internal static string PortValidatorMixedCombinationMessage { + get { + return ResourceManager.GetString("PortValidatorMixedCombinationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected an array of type {1} but received a nested array.. + /// + internal static string PortValidatorNestedArrayMessage { + get { + return ResourceManager.GetString("PortValidatorNestedArrayMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected type {1} but received null.. + /// + internal static string PortValidatorNullValueMessage { + get { + return ResourceManager.GetString("PortValidatorNullValueMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected an array of type {1} but received a single value.. + /// + internal static string PortValidatorSingleValueInsteadOfArrayMessage { + get { + return ResourceManager.GetString("PortValidatorSingleValueInsteadOfArrayMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input {0} expected type {1} but received {2}.. + /// + internal static string PortValidatorWrongTypeMessage { + get { + return ResourceManager.GetString("PortValidatorWrongTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + internal static string TypePortName { + get { + return ResourceManager.GetString("TypePortName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TypeDefinition as a string. + /// + internal static string TypePortTooltip { + get { + return ResourceManager.GetString("TypePortTooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input Type must be of type string. + /// + internal static string TypePortWarning { + get { + return ResourceManager.GetString("TypePortWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In. + /// + internal static string UnPackNodeDataInputName { + get { + return ResourceManager.GetString("UnPackNodeDataInputName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dictionary. + /// + internal static string UnPackNodeDataInputType { + get { + return ResourceManager.GetString("UnPackNodeDataInputType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Takes a dictionary and a TypeDefinition and returns the data matching the given TypeDefinition. + /// + internal static string UnPackNodeDescription { + get { + return ResourceManager.GetString("UnPackNodeDescription", resourceCulture); + } + } + } +} diff --git a/src/Libraries/PackingNodeModels/Properties/Resource.en-US.resx b/src/Libraries/PackingNodeModels/Properties/Resource.en-US.resx new file mode 100644 index 00000000000..7f25bbb9843 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Properties/Resource.en-US.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Creates a dictionary given a TypeDefinition and its corresponding input values + + + Out + + + Input {0} expected a single value of type {1} but received a list of values. + + + Input {0} expected an array of type {1} but received a mixed combination of single values and arrays. + + + Input {0} expected an array of type {1} but received a nested array. + + + Input {0} expected type {1} but received null. + + + Input {0} expected an array of type {1} but received a single value. + + + Input {0} expected type {1} but received {2}. + + + Type + + + TypeDefinition as a string + + + Input Type must be of type string + + + In + + + Dictionary + + + Takes a dictionary and a TypeDefinition and returns the data matching the given TypeDefinition + + \ No newline at end of file diff --git a/src/Libraries/PackingNodeModels/Properties/Resource.resx b/src/Libraries/PackingNodeModels/Properties/Resource.resx new file mode 100644 index 00000000000..7f25bbb9843 --- /dev/null +++ b/src/Libraries/PackingNodeModels/Properties/Resource.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Creates a dictionary given a TypeDefinition and its corresponding input values + + + Out + + + Input {0} expected a single value of type {1} but received a list of values. + + + Input {0} expected an array of type {1} but received a mixed combination of single values and arrays. + + + Input {0} expected an array of type {1} but received a nested array. + + + Input {0} expected type {1} but received null. + + + Input {0} expected an array of type {1} but received a single value. + + + Input {0} expected type {1} but received {2}. + + + Type + + + TypeDefinition as a string + + + Input Type must be of type string + + + In + + + Dictionary + + + Takes a dictionary and a TypeDefinition and returns the data matching the given TypeDefinition + + \ No newline at end of file diff --git a/src/Libraries/PackingNodeModels/TypeDefinition.cs b/src/Libraries/PackingNodeModels/TypeDefinition.cs new file mode 100644 index 00000000000..b1d079b9c16 --- /dev/null +++ b/src/Libraries/PackingNodeModels/TypeDefinition.cs @@ -0,0 +1,105 @@ +using Autodesk.DesignScript.Runtime; +using Dynamo.Graph.Nodes; +using Sprache; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PackingNodeModels +{ + /// + /// Defines a Type by its name and its properties. + /// + [IsVisibleInDynamoLibrary(false)] + public class TypeDefinition + { + public string Name { get; set; } + + public Dictionary Properties { get; set; } + } + + /// + /// Define a Type as a string name and whether it is a collection or not. + /// + [IsVisibleInDynamoLibrary(false)] + public class PropertyType + { + public string Type { get; set; } + public bool IsCollection { get; set; } + + public override string ToString() + { + return Type + (IsCollection ? "[]" : ""); + } + } + + /// + /// Typescript-like parser to create TypeDefinitions out of formatted strings. + /// i.e.: + /// "Type MyType { + /// property1 : String, + /// property2 : Float64, + /// property3 : Point[] + /// }" + /// + [IsVisibleInDynamoLibrary(false)] + internal class TypeDefinitionParser + { + private const string OpeningKeyword = "Type"; + + private static readonly Parser TypeKeyword = + from leading in Parse.WhiteSpace.Many() + from keyword in Parse.String(OpeningKeyword) + from trailing in Parse.WhiteSpace.Many() + select new string(keyword.ToArray()); + + private static readonly Parser TypeName = + from type in Parse.CharExcept(new[] { '{', '}', ',', '[', ']' }).AtLeastOnce() + from trailing in Parse.WhiteSpace.Many() + select new string(type.ToArray()).Trim(); + + private static readonly Parser> Property = + from leading in Parse.WhiteSpace.Many() + from firstLetter in Parse.Letter.AtLeastOnce() + from rest in Parse.LetterOrDigit.Many() + from seperator in Colon + from type in TypeName + from isCollection in Parse.String("[]").Optional() + from trailing in Parse.WhiteSpace.Many() + select new KeyValuePair(new string(firstLetter.ToArray()) + new string(rest.ToArray()), new PropertyType { Type = new string(type.ToArray()).Trim(), IsCollection = !isCollection.IsEmpty }); + + private static readonly Parser OpeningCurlyBracket = Operator('{'); + private static readonly Parser ClosingCurlyBracket = Operator('}'); + private static readonly Parser Colon = Operator(':'); + private static readonly Parser Comma = Operator(','); + + private static Parser Operator(char op) + { + return + from leading in Parse.WhiteSpace.Many() + from bracket in Parse.Char(op) + from trailing in Parse.WhiteSpace.Many() + select bracket; + } + + private static Parser TypeDefinition = + from keyword in TypeKeyword + from name in TypeName + from oB in OpeningCurlyBracket + from properties in Property.DelimitedBy(Comma) + from cB in ClosingCurlyBracket.End() + select new TypeDefinition { Name = name, Properties = properties.ToDictionary(pair => pair.Key, pair => pair.Value) }; + + /// + /// Parses a string and returns the corresponding TypeDefinition or throws a Sprache.ParseException. + /// + /// Formatted string definining a TypeDefinition + /// The parsed TypeDefinition + public static TypeDefinition ParseType(string text) + { + return TypeDefinition.Parse(text); + } + } +} diff --git a/src/Libraries/PackingNodeModels/UnPack/UnPack.cs b/src/Libraries/PackingNodeModels/UnPack/UnPack.cs new file mode 100644 index 00000000000..73f3c53482e --- /dev/null +++ b/src/Libraries/PackingNodeModels/UnPack/UnPack.cs @@ -0,0 +1,78 @@ +using Dynamo.Graph.Nodes; +using Newtonsoft.Json; +using PackingNodeModels.Properties; +using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PackingNodeModels.UnPack +{ + /// + /// Creates a node that takes in a Dictionary (or ArrayList of Dictionary) and a TypeDefinition to output the unpacked data from the dictionary. + /// + [NodeName("UnPack")] + [NodeCategory(BuiltinNodeCategories.CORE_PACKING)] + [NodeDescription("UnPackNodeDescription", typeof(Resource))] + [IsDesignScriptCompatible] + public class UnPack : PackingNode + { + public UnPack() + { + InPorts.Add(new PortModel(PortType.Input, this, new PortData(Resource.UnPackNodeDataInputName, Resource.UnPackNodeDataInputType))); + + ArgumentLacing = LacingStrategy.Longest; + } + + /// + /// Private constructor used for serialization. + /// + /// A collection of objects. + /// A collection of objects. + [JsonConstructor] + protected UnPack(IEnumerable inPorts, IEnumerable outPorts) + : base(inPorts, outPorts) + { + ArgumentLacing = LacingStrategy.Longest; + } + + protected override void RefreshTypeDefinitionPorts() + { + OutPorts.ToList().ForEach(portModel => OutPorts.Remove(portModel)); + + if (TypeDefinition != null) + { + foreach (var property in TypeDefinition.Properties) + { + OutPorts.Add(new PortModel(PortType.Output, this, new PortData(property.Key, property.Value.ToString()))); + } + } + } + + public override IEnumerable BuildOutputAst(List inputAstNodes) + { + var baseOutput = base.BuildOutputAst(inputAstNodes).ToList(); + + if (!IsValidInputState(inputAstNodes) || TypeDefinition == null) + { + baseOutput.Add(AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode())); + return baseOutput; + } + + UseLevelAndReplicationGuide(inputAstNodes); + + foreach (var property in TypeDefinition.Properties) + { + int index = TypeDefinition.Properties.ToList().IndexOf(property); + var functionCall = AstFactory.BuildFunctionCall( + new Func(DSCore.UnPackFunctions.UnPackOutputByKey), + new List { inputAstNodes[1], AstFactory.BuildStringNode(property.Key) }); + baseOutput.Add(AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(index), functionCall)); + } + + return baseOutput; + } + } +} diff --git a/src/Libraries/PackingNodeModels/packages.config b/src/Libraries/PackingNodeModels/packages.config new file mode 100644 index 00000000000..ac123f2e374 --- /dev/null +++ b/src/Libraries/PackingNodeModels/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/LibraryViewExtension/web/library/layoutSpecs.json b/src/LibraryViewExtension/web/library/layoutSpecs.json index 96c4a218f7f..c1098e8abf5 100644 --- a/src/LibraryViewExtension/web/library/layoutSpecs.json +++ b/src/LibraryViewExtension/web/library/layoutSpecs.json @@ -884,6 +884,21 @@ } ] }, + { + "text": "Packing", + "iconUrl": "", + "elementType": "category", + "include": [ + { + "path": "Core.Packing.Pack" + }, + { + "path": "Core.Packing.UnPack" + } + ], + "childElements": [ + ] + }, { "text": "ImportExport", "iconUrl": "./dist/resources/Category.ImportExport.svg", diff --git a/test/Libraries/CoreNodesTests/CoreNodesTests.csproj b/test/Libraries/CoreNodesTests/CoreNodesTests.csproj index 821ed4e0642..78577ae0ad3 100644 --- a/test/Libraries/CoreNodesTests/CoreNodesTests.csproj +++ b/test/Libraries/CoreNodesTests/CoreNodesTests.csproj @@ -77,6 +77,7 @@ + @@ -116,6 +117,10 @@ CoreNodes False + + {C0D6DEE5-5532-4345-9C66-4C00D7FDB8BE} + DesignScriptBuiltin + {ef879a10-041d-4c68-83e7-3192685f1bae} DynamoServices diff --git a/test/Libraries/CoreNodesTests/PackingTests.cs b/test/Libraries/CoreNodesTests/PackingTests.cs new file mode 100644 index 00000000000..3c4f03c22a3 --- /dev/null +++ b/test/Libraries/CoreNodesTests/PackingTests.cs @@ -0,0 +1,157 @@ +using DesignScript.Builtin; +using DSCore; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dynamo.Tests +{ + public class PackingTests + { + public class UnPackOutputByKeyMethod + { + [Test] + public void WhenDictionaryIsNull_ReturnsNull() + { + var result = UnPackFunctions.UnPackOutputByKey(null, "property1"); + + Assert.Null(result); + } + + [Test] + public void WhenDictionaryIsNotNull_ReturnsValueAtKey() + { + var dictionary = Dictionary.ByKeysValues(new List { "property1" }, new List { "value" }); + + var result = UnPackFunctions.UnPackOutputByKey(dictionary, "property1"); + + Assert.AreEqual("value", result); + } + } + + public class PackOutputAsDictionaryMethod + { + [Test] + public void WhenKeysAreNull_ShouldReturnNull() + { + var result = PackFunctions.PackOutputAsDictionary(null, new List { true }, 1); + + Assert.Null(result); + } + + [Test] + public void WhenKeysIsEmptyList_ShouldReturnNull() + { + var result = PackFunctions.PackOutputAsDictionary(new List(), new List { true }, 1); + + Assert.Null(result); + } + + [Test] + public void WhenIsCollectionIsNull_ShouldReturnNull() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1" }, null, 1); + + Assert.Null(result); + } + + [Test] + public void WhenIsCollectionIsEmpty_ShouldReturnNull() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1" }, new List(), 1); + + Assert.Null(result); + } + + [Test] + public void WhenDataIsNull_ShouldReturnNull() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1" }, new List { true }, null); + + Assert.Null(result); + } + + [Test] + public void WhenNoCollections_ReturnsDictionaryMatchingData() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { false, false }, new ArrayList { 1,2 }) as List>; + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].Count, Is.EqualTo(2)); + Assert.That(result[0]["property1"], Is.EqualTo(1)); + Assert.That(result[0]["property2"], Is.EqualTo(2)); + } + + [Test] + public void WhenNoCollectionsWithLacingDataOfSameSize_ReturnsDictionaryMatchingData() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { false, false }, new ArrayList { new ArrayList { 1, 3 }, new ArrayList { 2, 4 } }) as List>; + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result[0].Count, Is.EqualTo(2)); + Assert.That(result[0]["property1"], Is.EqualTo(1)); + Assert.That(result[0]["property2"], Is.EqualTo(2)); + Assert.That(result[1].Count, Is.EqualTo(2)); + Assert.That(result[1]["property1"], Is.EqualTo(3)); + Assert.That(result[1]["property2"], Is.EqualTo(4)); + } + + [Test] + public void WhenNoCollectionsWithLacingOfDifferentSize_ReturnsDictionaryWithDuplicateDataForShortestArray() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { false, false }, new ArrayList { new ArrayList { 1, 3, 5 }, new ArrayList { 2, 4 } }) as List>; + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0].Count, Is.EqualTo(2)); + Assert.That(result[0]["property1"], Is.EqualTo(1)); + Assert.That(result[0]["property2"], Is.EqualTo(2)); + Assert.That(result[1].Count, Is.EqualTo(2)); + Assert.That(result[1]["property1"], Is.EqualTo(3)); + Assert.That(result[1]["property2"], Is.EqualTo(4)); + Assert.That(result[2].Count, Is.EqualTo(2)); + Assert.That(result[2]["property1"], Is.EqualTo(5)); + Assert.That(result[2]["property2"], Is.EqualTo(4)); + } + + [Test] + public void WhenDataHasCollections_ReturnsDictionaryMatchingData() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { true, false }, new ArrayList { new ArrayList { 1, 3, 5 }, 10 }) as List>; + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0]["property1"], Is.EqualTo(new ArrayList { 1, 3, 5 })); + Assert.That(result[0]["property2"], Is.EqualTo(10)); + } + + [Test] + public void WhenDataHasCollectionsWithLacingOfSameSize_ReturnsDictionaryMatchingData() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { true, false }, new ArrayList { new ArrayList { new ArrayList { 1, 3, 5 }, new ArrayList { 2, 4, 6 } }, new ArrayList { 10, 15 } }) as List>; + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result[0]["property1"], Is.EqualTo(new ArrayList { 1, 3, 5 })); + Assert.That(result[0]["property2"], Is.EqualTo(10)); + Assert.That(result[1]["property1"], Is.EqualTo(new ArrayList { 2, 4, 6 })); + Assert.That(result[1]["property2"], Is.EqualTo(15)); + } + + [Test] + public void WhenDataHasCollectionsWithLacingOfDifferntSize_ReturnsDictionaryWithDuplicatedShortestValue() + { + var result = PackFunctions.PackOutputAsDictionary(new List { "property1", "property2" }, new List { true, false }, new ArrayList { new ArrayList { new ArrayList { 1, 3, 5 }, new ArrayList { 2, 4, 6 } }, new ArrayList { 10, 15, 20 } }) as List>; + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0]["property1"], Is.EqualTo(new ArrayList { 1, 3, 5 })); + Assert.That(result[0]["property2"], Is.EqualTo(10)); + Assert.That(result[1]["property1"], Is.EqualTo(new ArrayList { 2, 4, 6 })); + Assert.That(result[1]["property2"], Is.EqualTo(15)); + Assert.That(result[2]["property1"], Is.EqualTo(new ArrayList { 2, 4, 6 })); + Assert.That(result[2]["property2"], Is.EqualTo(20)); + } + } + } +} diff --git a/test/Libraries/PackingNodeTests/Pack/PackTests.cs b/test/Libraries/PackingNodeTests/Pack/PackTests.cs new file mode 100644 index 00000000000..9eb8b3da47a --- /dev/null +++ b/test/Libraries/PackingNodeTests/Pack/PackTests.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PackingNodeModels.Pack; +using VMDataBridge; +using System.Collections; +using NUnit.Framework; +using Dynamo.Graph.Nodes; +using PackingNodeModels.Pack.Validation; +using Moq; +using ProtoCore.AST.AssociativeAST; + +namespace Dynamo.Tests +{ + public class PackTests : DynamoModelTestBase + { + private string testFileWithPackUnPackNodes = Path.Combine(TestDirectory, @"core\PackingNode", "packingnodes.dyn"); + private const string typeDefinitionString = "Type A { prop:Type1 }"; + + private Pack GetPackNode() + { + return CurrentDynamoModel.CurrentWorkspace.Nodes.OfType().First(); + } + + private void HookUpTypeDefinition(Pack node, string typeString = typeDefinitionString) + { + node.InputNodes[0] = new Tuple(0, null); + DataBridge.BridgeData(node.GUID.ToString(), new ArrayList { typeString }); + } + + private void SetValidationManager(Pack node, IValidationManager validationManager) + { + var prop = node.GetType().GetField("validationManager", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + prop.SetValue(node, validationManager); + } + + public class Constructor : PackTests + { + [Test] + public void DefaultConstructor_ShouldAddDefaultOutport() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + + Assert.That(packNode.OutPorts.Count, Is.EqualTo(1)); + Assert.That(packNode.OutPorts[0].Name, Is.EqualTo("Out")); + Assert.That(packNode.OutPorts[0].ToolTip, Is.EqualTo("Dictionary")); + Assert.That(packNode.OutPorts[0].PortType, Is.EqualTo(PortType.Output)); + } + } + + public class DataBridgeCallBack : PackTests + { + [Test] + public void TypeDefinitionChange_ShouldAddPorts() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + + packNode.RequestScheduledTask += (action) => action(); + + Assert.That(packNode.InPorts.Count, Is.EqualTo(1)); + + HookUpTypeDefinition(packNode, "Type Something { prop1: Type1, prop2: Type2[] }"); + + Assert.That(packNode.InPorts.Count, Is.EqualTo(3)); + Assert.That(packNode.InPorts[1].Name, Is.EqualTo("prop1")); + Assert.That(packNode.InPorts[1].ToolTip, Is.EqualTo("Type1")); + Assert.That(packNode.InPorts[2].Name, Is.EqualTo("prop2")); + Assert.That(packNode.InPorts[2].ToolTip, Is.EqualTo("Type2[]")); + } + + [Test] + public void TypeDefinitionChangeToNull_ShouldRemovePorts() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + + packNode.RequestScheduledTask += (action) => action(); + + HookUpTypeDefinition(packNode, "Type Something { prop1: Type1, prop2: Type2[] }"); + + Assert.That(packNode.InPorts.Count, Is.EqualTo(3)); + + packNode.InputNodes.Clear(); + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { "" }); + + Assert.That(packNode.InPorts.Count, Is.EqualTo(1)); + } + } + + public class ValidateInputsMethod : PackTests + { + [Test] + public void NoCachedValues_ShouldCallHandleValidationForEveryValue() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + var validationManagerMock = new Mock(); + validationManagerMock.Setup(mock => mock.HandleValidation(It.IsAny>())); + validationManagerMock.Setup(mock => mock.Warnings).Returns(new List().AsReadOnly()); + SetValidationManager(packNode, validationManagerMock.Object); + + HookUpTypeDefinition(packNode, typeDefinitionString); + + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { typeDefinitionString, "someValue"}); + + validationManagerMock.Verify(mock => mock.HandleValidation(It.Is>(d => d.Count == 1)), Times.Once()); + } + + [Test] + public void WithCachedValues_ShouldCallHandleValidationForUnCachedValues() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + var validationManagerMock = new Mock(); + validationManagerMock.Setup(mock => mock.HandleValidation(It.IsAny>())); + validationManagerMock.Setup(mock => mock.Warnings).Returns(new List().AsReadOnly()); + SetValidationManager(packNode, validationManagerMock.Object); + var typeDefinitionString = "Type Something { prop1: Type1, prop2: Type2 }"; + + HookUpTypeDefinition(packNode, typeDefinitionString); + + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { typeDefinitionString, "someValue" }); + validationManagerMock.ResetCalls(); + + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { typeDefinitionString, "someValue", "newValue" }); + + validationManagerMock.Verify(mock => mock.HandleValidation(It.Is>(d => d.Count == 1)), Times.Once()); + } + + [Test] + public void WithCachedValuesWithEdits_ShouldCallHandleValidationForUnCachedValues() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + var validationManagerMock = new Mock(); + validationManagerMock.Setup(mock => mock.HandleValidation(It.IsAny>())); + validationManagerMock.Setup(mock => mock.Warnings).Returns(new List().AsReadOnly()); + SetValidationManager(packNode, validationManagerMock.Object); + var typeDefinitionString = "Type Something { prop1: Type1, prop2: Type2 }"; + + HookUpTypeDefinition(packNode, typeDefinitionString); + + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { typeDefinitionString, "someValue" }); + validationManagerMock.ResetCalls(); + + DataBridge.BridgeData(packNode.GUID.ToString(), new ArrayList { typeDefinitionString, "someNewValue", "newValue" }); + + validationManagerMock.Verify(mock => mock.HandleValidation(It.Is>(d => d.Count == 2)), Times.Once()); + } + + [Test] + public void WarningStateChange_ShouldCallOnNodeModified() + { + var onNodeModifiedCalled = false; + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + packNode.Modified += (x) => onNodeModifiedCalled = true; + + var validationManagerMock = new Mock(); + validationManagerMock.Setup(mock => mock.HandleValidation(It.IsAny>())); + validationManagerMock.SetupSequence(mock => mock.Warnings) + .Returns(new List().AsReadOnly()) + .Returns(new List() { new Validation() }.AsReadOnly()); + + SetValidationManager(packNode, validationManagerMock.Object); + + HookUpTypeDefinition(packNode, typeDefinitionString); + + Assert.True(onNodeModifiedCalled); + } + } + + public class BuildOutPutAstMethod : PackTests + { + [Test] + public void NullNodeList_ReturnsNullNode() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + packNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, packNode, new PortData(null, null)), new PortModel(PortType.Input, packNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(packNode); + + packNode.State = ElementState.Active; + + var output = packNode.BuildOutputAst(null); + + Assert.That(output.ElementAt(1), Is.InstanceOf()); + var binaryOutput = output.ElementAt(1) as BinaryExpressionNode; + Assert.That(binaryOutput.RightNode, Is.InstanceOf()); + } + + [Test] + public void NullNodeInsideNodeList_ReturnsNullNode() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + packNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, packNode, new PortData(null, null)), new PortModel(PortType.Input, packNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(packNode); + + packNode.State = ElementState.Active; + + var output = packNode.BuildOutputAst(new List { AstFactory.BuildStringNode("DST"), AstFactory.BuildNullNode() }); + + Assert.That(output.ElementAt(1), Is.InstanceOf()); + var binaryOutput = output.ElementAt(1) as BinaryExpressionNode; + Assert.That(binaryOutput.RightNode, Is.InstanceOf()); + } + + [Test] + public void NodeInWarningState_ReturnsNullNode() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + packNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, packNode, new PortData(null, null)), new PortModel(PortType.Input, packNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(packNode); + + packNode.State = ElementState.Warning; + + var output = packNode.BuildOutputAst(new List { AstFactory.BuildStringNode("DST"), AstFactory.BuildStringNode("a") }); + + Assert.That(output.ElementAt(1), Is.InstanceOf()); + var binaryOutput = output.ElementAt(1) as BinaryExpressionNode; + Assert.That(binaryOutput.RightNode, Is.InstanceOf()); + } + + [Test] + public void ValidDateAndInputs_ReturnFunctionCallNode() + { + OpenModel(testFileWithPackUnPackNodes); + + var packNode = GetPackNode(); + packNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, packNode, new PortData(null, null)), new PortModel(PortType.Input, packNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(packNode); + + packNode.State = ElementState.Active; + + var output = packNode.BuildOutputAst(new List { AstFactory.BuildStringNode("DST"), AstFactory.BuildStringNode("a") }); + + Assert.That(output.ElementAt(1), Is.InstanceOf()); + var binaryOutput = output.ElementAt(1) as BinaryExpressionNode; + + Assert.That(binaryOutput.RightNode, Is.InstanceOf()); + var identifierListNode = binaryOutput.RightNode as IdentifierListNode; + + Assert.That(identifierListNode.RightNode, Is.InstanceOf()); + } + } + } +} diff --git a/test/Libraries/PackingNodeTests/Pack/Validation/PortValidatorTests.cs b/test/Libraries/PackingNodeTests/Pack/Validation/PortValidatorTests.cs new file mode 100644 index 00000000000..12834a3a6d5 --- /dev/null +++ b/test/Libraries/PackingNodeTests/Pack/Validation/PortValidatorTests.cs @@ -0,0 +1,187 @@ +using Dynamo.Graph.Nodes; +using NUnit.Framework; +using PackingNodeModels; +using PackingNodeModels.Pack; +using PackingNodeModels.Pack.Validation; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dynamo.Tests +{ + public class PortValidatorTests + { + public class ValidateMethod + { + [Test] + public void WhenValueIsNull_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String" }); + var result = PortValidator.Validate(property, null, null); + + Assert.That(result, Is.EqualTo("Input prop1 expected type String but received null.")); + } + + public class WhenPropertyIsCollection + { + [Test] + public void WhenValueIsNotAnArrayList_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, "value", null); + + Assert.That(result, Is.EqualTo("Input prop1 expected an array of type String but received a single value.")); + } + + [Test] + public void WhenValueIsCollection_ValidData_ShouldReturnNull() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, new ArrayList { "value1", "value2", "value3" }, null); + + Assert.Null(result); + } + + [Test] + public void WhenValueIsACollectionOfSingleValuesAndCollections_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, new ArrayList { new ArrayList { "value1", "value2" }, "value3" }, null); + + Assert.That(result, Is.EqualTo("Input prop1 expected an array of type String but received a mixed combination of single values and arrays.")); + } + + [Test] + public void WhenValueIsCollectionOfCollection_ValidData_ShouldReturnNull() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, new ArrayList { new ArrayList { "value1", "value2" }, new ArrayList { "value3" } }, null); + + Assert.Null(result); + } + + [Test] + public void WhenValueIsCollectionOfCollection_OneInvalidValue_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, new ArrayList { new ArrayList { "value1", "value2" }, new ArrayList { 1 } }, null); + + Assert.NotNull(result); + } + + [Test] + public void WhenValueIsCollectionOfCollection_NestedArray_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = true }); + var result = PortValidator.Validate(property, new ArrayList { new ArrayList { "value1", "value2" }, new ArrayList { new ArrayList { "value3" } } }, null); + + Assert.That(result, Is.EqualTo("Input prop1 expected an array of type String but received a nested array.")); + } + } + + public class WhenPropertyIsNotCollection + { + [Test] + public void WhenValueIsArrayList_NestedArrays_ShouldReturnError() //Lacing with subvalues being collections + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = false }); + var result = PortValidator.Validate(property, new ArrayList { new ArrayList { "value1", "value2" } }, null); + + Assert.That(result, Is.EqualTo("Input prop1 expected a single value of type String but received a list of values.")); + } + + [Test] + public void WhenValueIsArrayList_ValidData_ShouldReturnNull() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = false }); + var result = PortValidator.Validate(property, new ArrayList { "value1", "value2" }, null); + + Assert.Null(result); + } + + [Test] + public void WhenValueIsSingle_Invalid_ShouldReturnError() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = false }); + var result = PortValidator.Validate(property, 1, null); + + Assert.NotNull(result); + } + + [Test] + public void WhenValueIsSingle_Valid_ShouldReturnError() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String", IsCollection = false }); + var result = PortValidator.Validate(property, "string", null); + + Assert.Null(result); + } + } + } + public class ValidateTypeMatch + { + [Test] + public void WhenTypeIsKnown_TypeMatch_ShouldReturnNull() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String" }); + var result = PortValidator.ValidateTypeMatch(property, "string", null); + + Assert.Null(result); + } + + [Test] + public void WhenTypeIsKnown_TypeDoesNotMatch_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "String" }); + var result = PortValidator.ValidateTypeMatch(property, 10, null); + + Assert.That(result, Is.EqualTo("Input prop1 expected type String but received System.Int32.")); + } + + [Test] + public void WhenTypeIsUnknown_HookedToNonPackNode_ShouldReturnErrorMessage() + { + var property = new KeyValuePair("prop1", new PropertyType { Type = "Something" }); + var portModel = new PortModel(PortType.Input, null, new PortData("prop1","")); + var ownerPortModel = new PortModel(PortType.Output, null, new PortData("out", "")); + portModel.Connectors.Add(new Graph.Connectors.ConnectorModel(ownerPortModel, portModel, Guid.NewGuid())); + var result = PortValidator.ValidateTypeMatch(property, 10, portModel); + + Assert.That(result, Is.EqualTo("Input prop1 expected type Something but received System.Int32.")); + } + + [Test] + public void WhenTypeIsUnknown_HookedToPackNodeButWrongMatch_ShouldReturnErrorMessage() + { + var previousPack = new Pack(); + previousPack.GetType().GetProperty("TypeDefinition").SetValue(previousPack, new TypeDefinition { Name = "NotSomething" }); + + var property = new KeyValuePair("prop1", new PropertyType { Type = "Something" }); + var portModel = new PortModel(PortType.Input, null, new PortData("prop1", "")); + var ownerPortModel = new PortModel(PortType.Output, previousPack, new PortData("out", "")); + portModel.Connectors.Add(new Graph.Connectors.ConnectorModel(ownerPortModel, portModel, Guid.NewGuid())); + var result = PortValidator.ValidateTypeMatch(property, 10, portModel); + + Assert.That(result, Is.EqualTo("Input prop1 expected type Something but received NotSomething.")); + } + + [Test] + public void WhenTypeIsUnknown_HookedToPackNodeWithTypeMatch_ShouldReturnNull() + { + var previousPack = new Pack(); + previousPack.GetType().GetProperty("TypeDefinition").SetValue(previousPack, new TypeDefinition { Name = "Something" }); + + var property = new KeyValuePair("prop1", new PropertyType { Type = "Something" }); + var portModel = new PortModel(PortType.Input, null, new PortData("prop1", "")); + var ownerPortModel = new PortModel(PortType.Output, previousPack, new PortData("out", "")); + portModel.Connectors.Add(new Graph.Connectors.ConnectorModel(ownerPortModel, portModel, Guid.NewGuid())); + var result = PortValidator.ValidateTypeMatch(property, 10, portModel); + + Assert.Null(result); + } + } + } +} diff --git a/test/Libraries/PackingNodeTests/Pack/Validation/ValidationManagerTests.cs b/test/Libraries/PackingNodeTests/Pack/Validation/ValidationManagerTests.cs new file mode 100644 index 00000000000..4ee782ab0c0 --- /dev/null +++ b/test/Libraries/PackingNodeTests/Pack/Validation/ValidationManagerTests.cs @@ -0,0 +1,133 @@ +using Dynamo.Graph.Nodes; +using NUnit.Framework; +using PackingNodeModels; +using PackingNodeModels.Pack; +using PackingNodeModels.Pack.Validation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dynamo.Tests +{ + public class ValidationManagerTests + { + private Pack GetNode(TypeDefinition type = null) + { + var pack = new Pack(); + + if (type == null) + { + type = new TypeDefinition(); + type.Name = "Something"; + type.Properties = new Dictionary { { "prop1", new PropertyType { Type = "String" } } }; + pack.InPorts.Add(new Graph.Nodes.PortModel(PortType.Input, pack, new PortData("prop1", ""))); + pack.InPorts[1].Connectors.Add(new Graph.Connectors.ConnectorModel(pack.InPorts[1], pack.InPorts[1], Guid.NewGuid())); + } + + + var prop = pack.GetType().GetProperty("TypeDefinition"); + prop.SetValue(pack, type); + + return pack; + } + + public class HandleValidation: ValidationManagerTests + { + [Test] + public void WhenInvalidDataIsGiven_AddsAWarningToNode() + { + var pack = GetNode(); + var validationManager = new ValidationManager(pack); + + var data = new Dictionary { { 1, 10 } }; //Should be a string value. + + validationManager.HandleValidation(data); + + Assert.That(pack.State, Is.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(1)); + } + + [Test] + public void WhenInvalidDataIsDisconnected_ShouldClearWarnings() + { + var pack = GetNode(); + var validationManager = new ValidationManager(pack); + + var data = new Dictionary { { 1, 10 } }; //Should be a string value. + + validationManager.HandleValidation(data); + + try + { + pack.InPorts[1].Connectors.Clear(); + } + catch { } + + var data2 = new Dictionary { { 1, null } }; + + validationManager.HandleValidation(data2); + + Assert.That(pack.State, Is.Not.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(0)); + } + + [Test] + public void WhenValidDataIsConnected_ShouldNotAddWarnings() + { + var pack = GetNode(); + var validationManager = new ValidationManager(pack); + + var data = new Dictionary { { 1, "String" } }; + + validationManager.HandleValidation(data); + + Assert.That(pack.State, Is.Not.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(0)); + } + + [Test] + public void WhenValidDataIsConnectedOnPreviouslyInvalidValue_ShouldClearWarnings() + { + var pack = GetNode(); + var validationManager = new ValidationManager(pack); + + var data = new Dictionary { { 1, 10 } }; //Should be a string value + + validationManager.HandleValidation(data); + + Assert.That(pack.State, Is.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(1)); + + var data2 = new Dictionary { { 1, "String" } }; + + validationManager.HandleValidation(data2); + + Assert.That(pack.State, Is.Not.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(0)); + } + + [Test] + public void WhenInValidDataIsConnectedOnPreviouslyInvalidValue_ShouldKeepWarnings() + { + var pack = GetNode(); + var validationManager = new ValidationManager(pack); + + var data = new Dictionary { { 1, 10 } }; //Should be a string value + + validationManager.HandleValidation(data); + + Assert.That(pack.State, Is.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(1)); + + var data2 = new Dictionary { { 1, null } }; + + validationManager.HandleValidation(data2); + + Assert.That(pack.State, Is.EqualTo(ElementState.PersistentWarning)); + Assert.That(validationManager.Warnings.Count, Is.EqualTo(1)); + } + } + } +} diff --git a/test/Libraries/PackingNodeTests/PackingNodeTests.cs b/test/Libraries/PackingNodeTests/PackingNodeTests.cs new file mode 100644 index 00000000000..f9c1e8ced5f --- /dev/null +++ b/test/Libraries/PackingNodeTests/PackingNodeTests.cs @@ -0,0 +1,233 @@ +using Dynamo.Graph.Nodes; +using NUnit.Framework; +using PackingNodeModels; +using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using VMDataBridge; + +namespace Dynamo.Tests +{ + public class PackingNodeTests : DynamoModelTestBase + { + private string testFileWithPackUnPackNodes = Path.Combine(TestDirectory, @"core\PackingNode", "packingnodes.dyn"); + private const string typeDefinitionString = "Type A { prop:Type1 }"; + + private PackingNode GetPackingNode() + { + return CurrentDynamoModel.CurrentWorkspace.Nodes.OfType().First(); + } + + private void HookUpTypeDefinition(PackingNode node, string typeString = typeDefinitionString) + { + node.InputNodes[0] = new Tuple(0, null); + DataBridge.BridgeData(node.GUID.ToString(), new ArrayList { typeString }); + } + + public class Constructor : PackingNodeTests + { + [Test] + public void DefaultCtor_ShouldAddTypeDefinitionInport() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.InPorts.Count, Is.GreaterThan(0)); + Assert.That(packingNode.InPorts[0].Name, Is.EqualTo("Type")); + Assert.That(packingNode.InPorts[0].ToolTip, Is.EqualTo("TypeDefinition as a string")); + Assert.That(packingNode.InPorts[0].PortType, Is.EqualTo(PortType.Input)); + } + } + + public class TypeDefinitionProperty : PackingNodeTests + { + [Test] + public void SettingValue_ShouldClearErrorsAndWarnings() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + packingNode.Error("some error"); + + Assert.True(packingNode.IsInErrorState); + + HookUpTypeDefinition(packingNode); + + Assert.False(packingNode.IsInErrorState); + } + + [Test] + public void SettingValue_ShouldRequestScheduledTask_WithRefreshTypeDefinitionPortsMethod() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + var requested = false; + var methodName = ""; + packingNode.RequestScheduledTask += (action) => { methodName = action.Method.Name; requested = true; }; + + HookUpTypeDefinition(packingNode); + + Assert.IsTrue(requested); + Assert.AreEqual("RefreshTypeDefinitionPorts", methodName); + } + } + + public class IsInValidStateProperty : PackingNodeTests + { + [Test] + public void Getter_ShouldConjunctErrorStateAndWarningStates() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + packingNode.State = ElementState.Error; + Assert.False(packingNode.IsInValidState); + + packingNode.State = ElementState.Warning; + Assert.False(packingNode.IsInValidState); + + packingNode.State = ElementState.PersistentWarning; + Assert.False(packingNode.IsInValidState); + + packingNode.State = ElementState.Active; + Assert.True(packingNode.IsInValidState); + } + } + + public class BuildOutputAstMethod : PackingNodeTests + { + [Test] + public void ShouldReturnDataBridgeAst() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + var astOutput = packingNode.BuildOutputAst(null); + + Assert.That(astOutput.Count(), Is.GreaterThanOrEqualTo(1)); + Assert.That(astOutput.ElementAt(0), Is.InstanceOf()); + + var binaryExpressionNode = astOutput.ElementAt(0) as BinaryExpressionNode; + Assert.That(binaryExpressionNode.RightNode, Is.InstanceOf()); + + var identifierListNode = binaryExpressionNode.RightNode as IdentifierListNode; + Assert.That(identifierListNode.RightNode, Is.InstanceOf()); + + var functionCallNode = identifierListNode.RightNode as FunctionCallNode; + Assert.That("BridgeData", Is.EqualTo(functionCallNode.Function.Name)); + } + } + + public class DataBridgeCallBackMethod : PackingNodeTests + { + [Test] + public void InputNodesCountIsZero_ShouldSetTypeDefinitionToNull() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.InputNodes.Count, Is.EqualTo(0)); + + DataBridge.BridgeData(packingNode.GUID.ToString(), new ArrayList { "" }); + + Assert.IsNull(packingNode.TypeDefinition); + } + + [Test] + public void InputNodesIndex0NotDefined_ShouldSetTypeDefinitionToNull() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.InputNodes.Count, Is.EqualTo(0)); + + packingNode.InputNodes[1] = new Tuple(1, null); + + DataBridge.BridgeData(packingNode.GUID.ToString(), new ArrayList { "" }); + + Assert.IsNull(packingNode.TypeDefinition); + } + + [Test] + public void InputNodesIndex0IsNull_ShouldSetTypeDefinitionToNull() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.InputNodes.Count, Is.EqualTo(0)); + + packingNode.InputNodes[0] = null; + + DataBridge.BridgeData(packingNode.GUID.ToString(), new ArrayList { "" }); + + Assert.IsNull(packingNode.TypeDefinition); + } + + [Test] + public void TypeValueIsNotString_ShouldAddAWarning() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.State, Is.Not.EqualTo(ElementState.PersistentWarning)); + + packingNode.InputNodes[0] = new Tuple(0, null); + + DataBridge.BridgeData(packingNode.GUID.ToString(), new ArrayList { 1 }); + + Assert.False(packingNode.IsInValidState); + Assert.That(packingNode.State, Is.EqualTo(ElementState.PersistentWarning)); + } + + [Test] + public void TypeValueIsInvalidTypeString_ShouldAddAWarning() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.That(packingNode.State, Is.Not.EqualTo(ElementState.PersistentWarning)); + + packingNode.InputNodes[0] = new Tuple(0, null); + + DataBridge.BridgeData(packingNode.GUID.ToString(), new ArrayList { "some invalid type definition string" }); + + Assert.False(packingNode.IsInValidState); + Assert.That(packingNode.State, Is.EqualTo(ElementState.PersistentWarning)); + } + + [Test] + public void TypeValueIsValidTypeString_ShouldUpdateTypeDefinition() + { + OpenModel(testFileWithPackUnPackNodes); + + var packingNode = GetPackingNode(); + + Assert.Null(packingNode.TypeDefinition); + + HookUpTypeDefinition(packingNode, "Type Something { prop1: Type1, prop2: Type2[] }"); + + Assert.That(packingNode.TypeDefinition.Name, Is.EqualTo("Something")); + Assert.That(packingNode.TypeDefinition.Properties.Count, Is.EqualTo(2)); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(0).Key, Is.EqualTo("prop1")); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(0).Value.Type, Is.EqualTo("Type1")); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(0).Value.IsCollection, Is.EqualTo(false)); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(1).Key, Is.EqualTo("prop2")); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(1).Value.Type, Is.EqualTo("Type2")); + Assert.That(packingNode.TypeDefinition.Properties.ElementAt(1).Value.IsCollection, Is.EqualTo(true)); + } + } + } +} diff --git a/test/Libraries/PackingNodeTests/PackingNodeTests.csproj b/test/Libraries/PackingNodeTests/PackingNodeTests.csproj new file mode 100644 index 00000000000..2a732a47e9e --- /dev/null +++ b/test/Libraries/PackingNodeTests/PackingNodeTests.csproj @@ -0,0 +1,86 @@ + + + + + + + + Debug + AnyCPU + {79F2ADE0-11C4-47F7-9CE4-ACFEFC25CAAF} + Library + Properties + PackingNodeTests + PackingNodeTests + v4.5 + 512 + + + true + full + false + $(OutputPath) + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + $(OutputPath) + TRACE + prompt + 4 + + + + ..\..\..\src\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll + False + True + + + False + ..\..\..\extern\NUnit\nunit.framework.dll + False + + + + + + + + + + + + + + + + + + + + + {7858FA8C-475F-4B8E-B468-1F8200778CF8} + DynamoCore + + + {7A9E0314-966F-4584-BAA3-7339CBB849D1} + ProtoCore + + + {d915633b-8bd6-4860-b0a7-4d689df445fa} + PackingNodeModels + + + {ccb6e56b-2da1-4eba-a1f9-e8510e129d12} + VMDataBridge + + + {472084ed-1067-4b2c-8737-3839a6143eb2} + DynamoCoreTests + + + + \ No newline at end of file diff --git a/test/Libraries/PackingNodeTests/Properties/AssemblyInfo.cs b/test/Libraries/PackingNodeTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..8cd1cbbcad9 --- /dev/null +++ b/test/Libraries/PackingNodeTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PackingNodeTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PackingNodeTests")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("79f2ade0-11c4-47f7-9ce4-acfefc25caaf")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/Libraries/PackingNodeTests/UnPack/UnPackTests.cs b/test/Libraries/PackingNodeTests/UnPack/UnPackTests.cs new file mode 100644 index 00000000000..6773473ae25 --- /dev/null +++ b/test/Libraries/PackingNodeTests/UnPack/UnPackTests.cs @@ -0,0 +1,124 @@ +using Dynamo.Graph.Nodes; +using NUnit.Framework; +using PackingNodeModels.UnPack; +using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VMDataBridge; + +namespace Dynamo.Tests +{ + public class UnPackTests : DynamoModelTestBase + { + private string testFileWithPackUnPackNodes = Path.Combine(TestDirectory, @"core\PackingNode", "packingnodes.dyn"); + private const string typeDefinitionString = "Type A { prop:Type1 }"; + + private UnPack GetUnPackNode() + { + return CurrentDynamoModel.CurrentWorkspace.Nodes.OfType().First(); + } + + private void HookUpTypeDefinition(UnPack node, string typeString = typeDefinitionString) + { + node.InputNodes[0] = new Tuple(0, null); + DataBridge.BridgeData(node.GUID.ToString(), new ArrayList { typeString }); + } + + public class Constructor : UnPackTests + { + [Test] + public void ArgumentLacing_SetToLongest() + { + var node = new UnPack(); + Assert.That(node.ArgumentLacing, Is.EqualTo(LacingStrategy.Longest)); + } + } + + public class DataBridgeCallBack : UnPackTests + { + [Test] + public void TypeDefinitionChange_ShouldAddPorts() + { + OpenModel(testFileWithPackUnPackNodes); + + var unpackNode = GetUnPackNode(); + + unpackNode.RequestScheduledTask += (action) => action(); + + Assert.That(unpackNode.OutPorts.Count, Is.EqualTo(0)); + + HookUpTypeDefinition(unpackNode, "Type Something { prop1: Type1, prop2: Type2[] }"); + + Assert.That(unpackNode.OutPorts.Count, Is.EqualTo(2)); + Assert.That(unpackNode.OutPorts[0].Name, Is.EqualTo("prop1")); + Assert.That(unpackNode.OutPorts[0].ToolTip, Is.EqualTo("Type1")); + Assert.That(unpackNode.OutPorts[1].Name, Is.EqualTo("prop2")); + Assert.That(unpackNode.OutPorts[1].ToolTip, Is.EqualTo("Type2[]")); + } + + [Test] + public void TypeDefinitionChangeToNull_ShouldRemovePorts() + { + OpenModel(testFileWithPackUnPackNodes); + + var unpackNode = GetUnPackNode(); + + unpackNode.RequestScheduledTask += (action) => action(); + + HookUpTypeDefinition(unpackNode, "Type Something { prop1: Type1, prop2: Type2[] }"); + + Assert.That(unpackNode.OutPorts.Count, Is.EqualTo(2)); + + unpackNode.InputNodes.Clear(); + DataBridge.BridgeData(unpackNode.GUID.ToString(), new ArrayList { "" }); + + Assert.That(unpackNode.OutPorts.Count, Is.EqualTo(0)); + } + } + + public class BuildOutPutAstMethod : UnPackTests + { + [Test] + public void NullNodeInsideNodeList_ReturnsNullNode() + { + OpenModel(testFileWithPackUnPackNodes); + + var unpackNode = GetUnPackNode(); + unpackNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, unpackNode, new PortData(null, null)), new PortModel(PortType.Input, unpackNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(unpackNode); + + unpackNode.State = ElementState.Active; + + var output = unpackNode.BuildOutputAst(new List { AstFactory.BuildStringNode("DST"), AstFactory.BuildNullNode() }); + + Assert.That(output.ElementAt(1), Is.InstanceOf()); + var binaryOutput = output.ElementAt(1) as BinaryExpressionNode; + Assert.That(binaryOutput.RightNode, Is.InstanceOf()); + } + + [Test] + public void ValidData_ShouldOutPutOneFunctionCallPerTypeProperty() + { + OpenModel(testFileWithPackUnPackNodes); + + var unpackNode = GetUnPackNode(); + unpackNode.RequestScheduledTask += (action) => action(); + unpackNode.InPorts[0].Connectors.Add(new Graph.Connectors.ConnectorModel(new PortModel(PortType.Input, unpackNode, new PortData(null, null)), new PortModel(PortType.Input, unpackNode, new PortData(null, null)), Guid.NewGuid())); + + HookUpTypeDefinition(unpackNode, "Type Something { prop1: Type1, prop2: Type2[]}"); + + var output = unpackNode.BuildOutputAst(new List { AstFactory.BuildStringNode("DST"), AstFactory.BuildStringNode("In") }); + + Assert.That(output.Count(), Is.EqualTo(3)); //1 DataBridge and 2 properties; + Assert.That(((output.ElementAt(1) as BinaryExpressionNode).RightNode as IdentifierListNode).RightNode, Is.InstanceOf()); + Assert.That(((output.ElementAt(2) as BinaryExpressionNode).RightNode as IdentifierListNode).RightNode, Is.InstanceOf()); + } + } + } +} diff --git a/test/core/PackingNode/packingnodes.dyn b/test/core/PackingNode/packingnodes.dyn new file mode 100644 index 00000000000..7f1a901be1c --- /dev/null +++ b/test/core/PackingNode/packingnodes.dyn @@ -0,0 +1,121 @@ +{ + "Uuid": "1e3be5e6-78bb-4665-af10-7dd3c94e217c", + "IsCustomNode": false, + "Description": null, + "Name": "packingnodes", + "ElementResolver": { + "ResolutionMap": {} + }, + "Inputs": [], + "Outputs": [], + "Nodes": [ + { + "ConcreteType": "PackingNodeModels.Pack.Pack, PackingNodeModels", + "NodeType": "ExtensionNode", + "Id": "19e41724d5bd494ca0fa2c104f34b3ff", + "Inputs": [ + { + "Id": "829d359c072c47f89bf8740359a019fd", + "Name": "Type", + "Description": "TypeDefinition as a string", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "65b92271fcfd4bedad04fa1350649810", + "Name": "Out", + "Description": "Dictionary", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Pack Node" + }, + { + "ConcreteType": "PackingNodeModels.UnPack.UnPack, PackingNodeModels", + "NodeType": "ExtensionNode", + "Id": "71f86e45fe1c4100a6cdec7506bc0119", + "Inputs": [ + { + "Id": "c34caedab5aa44768a40e48dcabb761c", + "Name": "Type", + "Description": "TypeDefinition as a string", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + }, + { + "Id": "dbcda1643c8040c38b6754a9a1bb7c0c", + "Name": "In", + "Description": "Dictionary", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [], + "Replication": "Longest", + "Description": "UnPack Node" + } + ], + "Connectors": [], + "Dependencies": [], + "Bindings": [], + "View": { + "Dynamo": { + "ScaleFactor": 1.0, + "HasRunWithoutCrash": true, + "IsVisibleInDynamoLibrary": true, + "Version": "2.1.0.5634", + "RunType": "Automatic", + "RunPeriod": "1000" + }, + "Camera": { + "Name": "Background Preview", + "EyeX": -17.0, + "EyeY": 24.0, + "EyeZ": 50.0, + "LookX": 12.0, + "LookY": -13.0, + "LookZ": -58.0, + "UpX": 0.0, + "UpY": 1.0, + "UpZ": 0.0 + }, + "NodeViews": [ + { + "ShowGeometry": true, + "Name": "Pack", + "Id": "19e41724d5bd494ca0fa2c104f34b3ff", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Excluded": false, + "X": 455.5, + "Y": 350.0 + }, + { + "ShowGeometry": true, + "Name": "UnPack", + "Id": "71f86e45fe1c4100a6cdec7506bc0119", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Excluded": false, + "X": 777.5, + "Y": 344.0 + } + ], + "Annotations": [], + "X": 0.0, + "Y": 0.0, + "Zoom": 1.0 + } +} \ No newline at end of file