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