From cc33b15643a8ab269d4c16fca2dcea358d3fd5c3 Mon Sep 17 00:00:00 2001 From: Meinte Boersma Date: Mon, 8 Jul 2024 14:51:06 +0200 Subject: [PATCH 1/4] implement naive reference finder + update changelog for issue #7 --- CHANGELOG.md | 12 ++- .../core/Utilities/ReferenceExtensions.cs | 90 +++++++++++++++++++ .../models/ExampleModels.cs | 5 +- .../tests/ReferenceExtensionsTests.cs | 80 +++++++++++++++++ 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs create mode 100644 test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bed192..b8ce4b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,17 @@ and this project adheres _loosely_ to [Semantic Versioning](https://semver.org/s - Generate interface for each factory; factory implementation methods are `virtual` now. -## Changed +### Added + +- Add extensions class `ReferenceExtensions` (in namespace `TL.LDM.Language.Extensions`) with extension methods to deal with references: + - `.AllReferenceValues()` finds all references within the forest with trunks ``. + - `.AllIncomingReferencesWithin()` finds all references _to_ the `` within the forest with trunks ``. + +### Fixed + +- Fix bug ([issue #7](https://github.com/LionWeb-io/lionweb-csharp/issues/7)) in `Textualizer`: don't crash on unset `name` properties of `INamed`s. + +### Changed - Released as open source under the Apache-2.0 license. - Set up CI using GitHub Actions. diff --git a/src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs b/src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs new file mode 100644 index 00000000..d52633a8 --- /dev/null +++ b/src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs @@ -0,0 +1,90 @@ +// Copyright 2024 TRUMPF Laser SE and other contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-FileCopyrightText: 2024 TRUMPF Laser SE and other contributors +// SPDX-License-Identifier: Apache-2.0 + +// ReSharper disable once CheckNamespace +namespace LionWeb.Core.Utilities; + +using M3; + +/// +/// Information about a source and target node related through a . +/// +/// The source of the reference (relation) +/// The feature that has the reference to the target node +/// The index within the (intrinsically-ordered) multi-value of the reference feature on the of the reference, +/// or null if the reference feature is not multivalued +/// The target of the reference (relation) +/// +// ReSharper disable NotAccessedPositionalProperty.Global +public record ReferenceValue(IReadableNode SourceNode, Reference Reference, int? Index, IReadableNode TargetNode); + +/// +/// Extension methods to deal with references, i.e. values of features. +/// +public static class ReferenceExtensions +{ + private static IEnumerable GatherReferenceValues(IReadableNode sourceNode, Reference reference) + => reference.Multiple + ? (sourceNode.Get(reference) as IEnumerable ?? []).Select((targetNode, index) => + new ReferenceValue(sourceNode, reference, index, targetNode)) + : [new ReferenceValue(sourceNode, reference, null, (sourceNode.Get(reference) as IReadableNode)!)]; + + /// + /// Finds all references within the given , as s. + /// To search within all nodes under a collection of root nodes, + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// + /// The s that are searched for references + /// An enumeration of references, as s. + public static IEnumerable ReferenceValues(IEnumerable scope) + => scope + .SelectMany(sourceNode => // for all nodes in the scope: + sourceNode + .CollectAllSetFeatures().OfType() // for all set references: + .SelectMany(reference => GatherReferenceValues(sourceNode, reference)) // gather all reference values + ); + + /// + /// Finds all references coming into any of the given + /// within the given , as s. + /// To search within all nodes under a collection of root nodes, + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// + /// The target nodes for which the incoming references are searched + /// The s that are searched for references + /// An enumeration of references, as s. + public static IEnumerable FindIncomingReferences(IEnumerable targetNodes, + IEnumerable scope) + { + var targetNodesAsSet = new HashSet(targetNodes); + return ReferenceValues(scope) + .Where(referenceValue => targetNodesAsSet.Contains(referenceValue.TargetNode)); + } + + /// + /// Finds all references coming into the given + /// within the given , as s. + /// To search within all nodes under a collection of root nodes, + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// + /// A target + /// The s that form the scope of the search + /// An enumeration of references, as s. + public static IEnumerable FindIncomingReferences(IReadableNode targetNode, + IEnumerable scope) + => FindIncomingReferences([targetNode], scope); +} \ No newline at end of file diff --git a/test/LionWeb-CSharp-Test/models/ExampleModels.cs b/test/LionWeb-CSharp-Test/models/ExampleModels.cs index 72f3f896..8ede08e2 100644 --- a/test/LionWeb-CSharp-Test/models/ExampleModels.cs +++ b/test/LionWeb-CSharp-Test/models/ExampleModels.cs @@ -57,8 +57,9 @@ public static INode ExampleLine(Language lang) public static INode ExampleModel(Language lang) { - var geometry = lang.GetFactory().CreateNode(IdUtils.NewId(), lang.ClassifierByKey("key-Geometry")); - geometry.Set(lang.ClassifierByKey("key-Geometry").FeatureByKey("key-shapes"), new List{ExampleLine(lang)}); + var language = ShapesLanguage.Instance; + var geometry = language.GetFactory().CreateNode(IdUtils.NewId(), language.ClassifierByKey("key-Geometry")); + geometry.Set(language.ClassifierByKey("key-Geometry").FeatureByKey("key-shapes"), new List{ExampleLine(language)}); return geometry; } diff --git a/test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs b/test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs new file mode 100644 index 00000000..f0495a07 --- /dev/null +++ b/test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs @@ -0,0 +1,80 @@ +// Copyright 2024 TRUMPF Laser SE and other contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-FileCopyrightText: 2024 TRUMPF Laser SE and other contributors +// SPDX-License-Identifier: Apache-2.0 + +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable 1591 + +namespace LionWeb_CSharp_Test.tests; + +using Examples.Shapes.Dynamic; +using Examples.Shapes.M2; +using LionWeb.Core; +using LionWeb.Core.Utilities; + +[TestClass] +public class ReferenceExtensionsTests +{ + [TestMethod] + public void can_find_a_reference_from_a_feature_of_a_concept() + { + var language = ShapesLanguage.Instance; + var factory = language.GetFactory(); + var referenceGeometry = factory.CreateReferenceGeometry(); + + var geometry = (ExampleModels.ExampleModel(language) as Geometry)!; + referenceGeometry.AddShapes(geometry.Shapes); + + List rootNodes = [geometry, referenceGeometry]; + var refs = ReferenceExtensions.FindIncomingReferences(geometry.Shapes[0], rootNodes).ToList(); + Assert.AreEqual(1, refs.Count()); + var ref0 = refs.First(); + + var expectedReferenceValue = + new ReferenceValue(referenceGeometry, language.ReferenceGeometry_shapes, 0, geometry.Shapes[0]); + Assert.AreEqual(expectedReferenceValue, ref0); // (relies on value equality of C# record types) + + var allRefs = ReferenceExtensions.ReferenceValues(rootNodes).ToList(); + Assert.AreEqual(1, allRefs.Count()); + Assert.AreEqual(expectedReferenceValue, allRefs.First()); + } + + [TestMethod] + public void can_find_a_reference_from_an_annotation() + { + var language = ShapesLanguage.Instance; + var factory = language.GetFactory(); + + var circle = factory.CreateCircle(); + + var line = (ExampleModels.ExampleLine(language) as Line)!; + var bom = factory.CreateBillOfMaterials(); + bom.AddMaterials([circle]); + line.AddAnnotations([bom]); + + var geometry = factory.CreateGeometry(); + geometry.AddShapes([circle, line]); + + var refs = ReferenceExtensions.FindIncomingReferences(circle, [bom]).ToList(); + Assert.AreEqual(1, refs.Count()); + var ref0 = refs.First(); + + var expectedReferenceValue = + new ReferenceValue(bom, language.BillOfMaterials_materials, 0, circle); + Assert.AreEqual(expectedReferenceValue, ref0); // (relies on value equality of C# record types) + } +} \ No newline at end of file From c3a359fc8b9fbeab1473df19e081b5d9d3ae9143 Mon Sep 17 00:00:00 2001 From: Meinte Boersma Date: Wed, 10 Jul 2024 10:10:50 +0200 Subject: [PATCH 2/4] process Niko's review comments ! still TODO: add proposed unit tests --- CHANGELOG.md | 9 ++--- ...ferenceExtensions.cs => ReferenceUtils.cs} | 30 ++++++++++------ ...ensionsTests.cs => ReferenceUtilsTests.cs} | 35 +++++++++---------- 3 files changed, 39 insertions(+), 35 deletions(-) rename src/LionWeb-CSharp/core/Utilities/{ReferenceExtensions.cs => ReferenceUtils.cs} (78%) rename test/LionWeb-CSharp-Test/tests/{ReferenceExtensionsTests.cs => ReferenceUtilsTests.cs} (61%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ce4b8e..6b9a4b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,12 +22,9 @@ and this project adheres _loosely_ to [Semantic Versioning](https://semver.org/s ### Added - Generate interface for each factory; factory implementation methods are `virtual` now. - -### Added - -- Add extensions class `ReferenceExtensions` (in namespace `TL.LDM.Language.Extensions`) with extension methods to deal with references: - - `.AllReferenceValues()` finds all references within the forest with trunks ``. - - `.AllIncomingReferencesWithin()` finds all references _to_ the `` within the forest with trunks ``. +- Add utilities class `ReferenceUtils` (in namespace `LionWeb.Core.Utilities`) to deal with references: + - `ReferenceValues()` finds all references within all the given ``. + - `FindIncomingReferences(, )` finds all references _to_ (all) the `` within the given ``. ### Fixed diff --git a/src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs similarity index 78% rename from src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs rename to src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs index d52633a8..23e41397 100644 --- a/src/LionWeb-CSharp/core/Utilities/ReferenceExtensions.cs +++ b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs @@ -26,7 +26,7 @@ namespace LionWeb.Core.Utilities; /// The source of the reference (relation) /// The feature that has the reference to the target node /// The index within the (intrinsically-ordered) multi-value of the reference feature on the of the reference, -/// or null if the reference feature is not multivalued +/// or null if the reference feature is not multivalued /// The target of the reference (relation) /// // ReSharper disable NotAccessedPositionalProperty.Global @@ -35,18 +35,27 @@ public record ReferenceValue(IReadableNode SourceNode, Reference Reference, int? /// /// Extension methods to deal with references, i.e. values of features. /// -public static class ReferenceExtensions +public static class ReferenceUtils { private static IEnumerable GatherReferenceValues(IReadableNode sourceNode, Reference reference) - => reference.Multiple - ? (sourceNode.Get(reference) as IEnumerable ?? []).Select((targetNode, index) => - new ReferenceValue(sourceNode, reference, index, targetNode)) - : [new ReferenceValue(sourceNode, reference, null, (sourceNode.Get(reference) as IReadableNode)!)]; + { + if (reference.Multiple) + { + var sourceNodes = sourceNode.Get(reference) as IEnumerable ?? []; + return sourceNodes + .Select((targetNode, index) => + new ReferenceValue(sourceNode, reference, index, targetNode) + ); + } + + // singular: + return [new ReferenceValue(sourceNode, reference, null, (sourceNode.Get(reference) as IReadableNode)!)]; + } /// /// Finds all references within the given , as s. /// To search within all nodes under a collection of root nodes, - /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. /// /// The s that are searched for references /// An enumeration of references, as s. @@ -54,7 +63,8 @@ public static IEnumerable ReferenceValues(IEnumerable scope .SelectMany(sourceNode => // for all nodes in the scope: sourceNode - .CollectAllSetFeatures().OfType() // for all set references: + .CollectAllSetFeatures() // for all set features + .OfType() // that are references: .SelectMany(reference => GatherReferenceValues(sourceNode, reference)) // gather all reference values ); @@ -62,7 +72,7 @@ public static IEnumerable ReferenceValues(IEnumerable /// within the given , as s. /// To search within all nodes under a collection of root nodes, - /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. /// /// The target nodes for which the incoming references are searched /// The s that are searched for references @@ -79,7 +89,7 @@ public static IEnumerable FindIncomingReferences(IEnumerable /// within the given , as s. /// To search within all nodes under a collection of root nodes, - /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. /// /// A target /// The s that form the scope of the search diff --git a/test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs similarity index 61% rename from test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs rename to test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs index f0495a07..08f95917 100644 --- a/test/LionWeb-CSharp-Test/tests/ReferenceExtensionsTests.cs +++ b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs @@ -27,7 +27,7 @@ namespace LionWeb_CSharp_Test.tests; using LionWeb.Core.Utilities; [TestClass] -public class ReferenceExtensionsTests +public class ReferenceUtilsTests { [TestMethod] public void can_find_a_reference_from_a_feature_of_a_concept() @@ -39,18 +39,15 @@ public void can_find_a_reference_from_a_feature_of_a_concept() var geometry = (ExampleModels.ExampleModel(language) as Geometry)!; referenceGeometry.AddShapes(geometry.Shapes); - List rootNodes = [geometry, referenceGeometry]; - var refs = ReferenceExtensions.FindIncomingReferences(geometry.Shapes[0], rootNodes).ToList(); - Assert.AreEqual(1, refs.Count()); - var ref0 = refs.First(); + List scope = [geometry, referenceGeometry]; + var expectedRefs = new List + { + new (referenceGeometry, language.ReferenceGeometry_shapes, 0, geometry.Shapes[0]) + }; - var expectedReferenceValue = - new ReferenceValue(referenceGeometry, language.ReferenceGeometry_shapes, 0, geometry.Shapes[0]); - Assert.AreEqual(expectedReferenceValue, ref0); // (relies on value equality of C# record types) - - var allRefs = ReferenceExtensions.ReferenceValues(rootNodes).ToList(); - Assert.AreEqual(1, allRefs.Count()); - Assert.AreEqual(expectedReferenceValue, allRefs.First()); + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.FindIncomingReferences(geometry.Shapes[0], scope).ToList()); + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.ReferenceValues(scope).ToList()); + // (also relies on value equality of C# record types) } [TestMethod] @@ -69,12 +66,12 @@ public void can_find_a_reference_from_an_annotation() var geometry = factory.CreateGeometry(); geometry.AddShapes([circle, line]); - var refs = ReferenceExtensions.FindIncomingReferences(circle, [bom]).ToList(); - Assert.AreEqual(1, refs.Count()); - var ref0 = refs.First(); - - var expectedReferenceValue = - new ReferenceValue(bom, language.BillOfMaterials_materials, 0, circle); - Assert.AreEqual(expectedReferenceValue, ref0); // (relies on value equality of C# record types) + CollectionAssert.AreEqual( + new List + { + new (bom, language.BillOfMaterials_materials, 0, circle) + }, + ReferenceUtils.FindIncomingReferences(circle, [bom]).ToList() + ); } } \ No newline at end of file From 9c4ff55118c6e27a1f7646ae413c351aa435a102 Mon Sep 17 00:00:00 2001 From: Meinte Boersma Date: Thu, 11 Jul 2024 09:37:48 +0200 Subject: [PATCH 3/4] add suggested unit tests --- .../core/Utilities/ReferenceUtils.cs | 1 + .../tests/ReferenceUtilsTests.cs | 160 +++++++++++++++++- 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs index 23e41397..01d9ff3c 100644 --- a/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs +++ b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs @@ -61,6 +61,7 @@ private static IEnumerable GatherReferenceValues(IReadableNode s /// An enumeration of references, as s. public static IEnumerable ReferenceValues(IEnumerable scope) => scope + .Distinct() .SelectMany(sourceNode => // for all nodes in the scope: sourceNode .CollectAllSetFeatures() // for all set features diff --git a/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs index 08f95917..33ada421 100644 --- a/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs +++ b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs @@ -24,13 +24,18 @@ namespace LionWeb_CSharp_Test.tests; using Examples.Shapes.Dynamic; using Examples.Shapes.M2; using LionWeb.Core; +using LionWeb.Core.M3; using LionWeb.Core.Utilities; +/// +/// Note: the assertions in this test class use , +/// and rely on value equality of C# record types such as . +/// [TestClass] public class ReferenceUtilsTests { [TestMethod] - public void can_find_a_reference_from_a_feature_of_a_concept() + public void finds_a_reference_from_a_feature_of_a_concept() { var language = ShapesLanguage.Instance; var factory = language.GetFactory(); @@ -47,11 +52,10 @@ public void can_find_a_reference_from_a_feature_of_a_concept() CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.FindIncomingReferences(geometry.Shapes[0], scope).ToList()); CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.ReferenceValues(scope).ToList()); - // (also relies on value equality of C# record types) } [TestMethod] - public void can_find_a_reference_from_an_annotation() + public void finds_a_reference_from_an_annotation() { var language = ShapesLanguage.Instance; var factory = language.GetFactory(); @@ -74,4 +78,154 @@ public void can_find_a_reference_from_an_annotation() ReferenceUtils.FindIncomingReferences(circle, [bom]).ToList() ); } + + [TestMethod] + public void finds_a_reference_to_itself() + { + var language = new DynamicLanguage("lang"); + var concept = new DynamicConcept("concept", language); + language.AddEntities([concept]); + + var selfRef = new DynamicReference("selfRef", concept); + concept.AddFeatures([selfRef]); + selfRef.Type = concept; + + var node = new DynamicNode("node", concept); + node.Set(selfRef, node); + + CollectionAssert.AreEqual( + new List + { + new (node, selfRef, null, node) + }, + ReferenceUtils.FindIncomingReferences(node, [node]).ToList() + ); + } + + [TestMethod] + public void finds_references_in_different_features_of_the_source() + { + var language = new DynamicLanguage("lang"); + var concept = new DynamicConcept("concept", language); + language.AddEntities([concept]); + + var ref1 = new DynamicReference("ref1", concept); + concept.AddFeatures([ref1]); + ref1.Type = concept; + + var ref2 = new DynamicReference("ref2", concept); + concept.AddFeatures([ref2]); + ref2.Type = concept; + + var targetNode = new DynamicNode("targetNode", concept); + var sourceNode = new DynamicNode("sourceNode", concept); + sourceNode.Set(ref1, targetNode); + sourceNode.Set(ref2, targetNode); + + CollectionAssert.AreEquivalent( + new List + { + new (sourceNode, ref1, null, targetNode), + new (sourceNode, ref2, null, targetNode), + }, + ReferenceUtils.FindIncomingReferences(targetNode, [sourceNode]).ToList() + ); + } + + [TestMethod] + public void finds_multiple_references_to_target_in_a_multivalued_feature_of_the_source() + { + var language = new DynamicLanguage("lang"); + var concept = new DynamicConcept("concept", language); + language.AddEntities([concept]); + + var myRef = new DynamicReference("myRef", concept); + concept.AddFeatures([myRef]); + myRef.Type = concept; + myRef.Multiple = true; + + var targetNode = new DynamicNode("targetNode", concept); + var sourceNode = new DynamicNode("sourceNode", concept); + sourceNode.Set(myRef, new List { targetNode, targetNode }); + + CollectionAssert.AreEquivalent( + new List + { + new (sourceNode, myRef, 0, targetNode), + new (sourceNode, myRef, 1, targetNode), + }, + ReferenceUtils.FindIncomingReferences(targetNode, [sourceNode]).ToList() + ); + } + + [TestMethod] + public void finds_references_among_multiple_sources_and_targets() + { + var language = new DynamicLanguage("lang"); + var concept = new DynamicConcept("concept", language); + language.AddEntities([concept]); + + var myRef = new DynamicReference("myRef", concept); + concept.AddFeatures([myRef]); + myRef.Type = concept; + + var sourceNode1 = new DynamicNode("sourceNode1", concept); + var sourceNode2 = new DynamicNode("sourceNode2", concept); + var targetNode1 = new DynamicNode("targetNode1", concept); + var targetNode2 = new DynamicNode("targetNode2", concept); + sourceNode1.Set(myRef, targetNode1); + sourceNode2.Set(myRef, targetNode2); + + CollectionAssert.AreEquivalent( + new List + { + new (sourceNode1, myRef, null, targetNode1), + new (sourceNode2, myRef, null, targetNode2), + }, + ReferenceUtils.FindIncomingReferences([targetNode1, targetNode2], [sourceNode1, sourceNode2]).ToList() + ); + } + + [TestMethod] + public void has_defined_behavior_for_duplicate_target_nodes() + { + var language = ShapesLanguage.Instance; + var factory = language.GetFactory(); + var referenceGeometry = factory.CreateReferenceGeometry(); + + var geometry = (ExampleModels.ExampleModel(language) as Geometry)!; + referenceGeometry.AddShapes(geometry.Shapes); + + List scope = [geometry, referenceGeometry]; + var expectedRefs = new List + { + new (referenceGeometry, language.ReferenceGeometry_shapes, 0, geometry.Shapes[0]) + }; + + var targetNode = geometry.Shapes[0]; + IEnumerable duplicateTargetNodes = [targetNode, targetNode]; + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.FindIncomingReferences(duplicateTargetNodes, scope).ToList()); + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.ReferenceValues(scope).ToList()); + } + + [TestMethod] + public void has_defined_behavior_when_duplicate_nodes_in_scope() + { + var language = ShapesLanguage.Instance; + var factory = language.GetFactory(); + var referenceGeometry = factory.CreateReferenceGeometry(); + + var geometry = (ExampleModels.ExampleModel(language) as Geometry)!; + referenceGeometry.AddShapes(geometry.Shapes); + + List scope = [geometry, referenceGeometry]; + var expectedRefs = new List + { + new (referenceGeometry, language.ReferenceGeometry_shapes, 0, geometry.Shapes[0]) + }; + + var duplicateScope = scope.Concat(scope); + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.FindIncomingReferences(geometry.Shapes[0], duplicateScope).ToList()); + CollectionAssert.AreEqual(expectedRefs, ReferenceUtils.ReferenceValues(scope).ToList()); + } } \ No newline at end of file From 67bdd75b9dc6c2e774762e8006ad43b96c1769c9 Mon Sep 17 00:00:00 2001 From: Meinte Boersma Date: Thu, 11 Jul 2024 13:19:57 +0200 Subject: [PATCH 4/4] switch from use of DynamicNode to generated test language + document behavior on duplicate scope and target nodes --- build/LionWeb-CSharp-Build/Generate.cs | 6 +- .../TestLanguagesDefinitions.cs | 19 ++ .../core/Utilities/ReferenceUtils.cs | 15 +- .../languages/generated/TinyRefLang.g.cs | 214 ++++++++++++++++++ .../tests/ReferenceUtilsTests.cs | 87 +++---- 5 files changed, 282 insertions(+), 59 deletions(-) create mode 100644 test/LionWeb-CSharp-Test/languages/generated/TinyRefLang.g.cs diff --git a/build/LionWeb-CSharp-Build/Generate.cs b/build/LionWeb-CSharp-Build/Generate.cs index 9efc4f07..d2b21615 100644 --- a/build/LionWeb-CSharp-Build/Generate.cs +++ b/build/LionWeb-CSharp-Build/Generate.cs @@ -47,6 +47,7 @@ DynamicLanguage[] DeserializeExternalLanguage(string name, params Language[] dep var testLanguagesDefinitions = new TestLanguagesDefinitions(); var aLang = testLanguagesDefinitions.ALang; var bLang = testLanguagesDefinitions.BLang; +var tinyRefLang = testLanguagesDefinitions.TinyRefLang; List names = [ @@ -59,14 +60,15 @@ DynamicLanguage[] DeserializeExternalLanguage(string name, params Language[] dep }, new (withEnumLanguage, "Examples.WithEnum.M2"), new (shapesLanguage, "Examples.Shapes.M2"), - new (testLanguagesDefinitions.ALang, "Examples.Circular.A") + new (aLang, "Examples.Circular.A") { NamespaceMappings = { [bLang] = "Examples.Circular.B" } }, - new (testLanguagesDefinitions.BLang, "Examples.Circular.B") + new (bLang, "Examples.Circular.B") { NamespaceMappings = { [aLang] = "Examples.Circular.A" } }, + new (tinyRefLang, "Examples.TinyRefLang"), ]; diff --git a/build/LionWeb-CSharp-Build/TestLanguagesDefinitions.cs b/build/LionWeb-CSharp-Build/TestLanguagesDefinitions.cs index e79b737d..bea3d735 100644 --- a/build/LionWeb-CSharp-Build/TestLanguagesDefinitions.cs +++ b/build/LionWeb-CSharp-Build/TestLanguagesDefinitions.cs @@ -24,6 +24,7 @@ public class TestLanguagesDefinitions { public readonly Language ALang; public readonly Language BLang; + public readonly Language TinyRefLang; public TestLanguagesDefinitions() { @@ -59,5 +60,23 @@ This is my ALang = aLang; BLang = bLang; + + + var tinyRefLang = new DynamicLanguage("id-TinyRefLang") { Key = "key-tinyRefLang", Name = "TinyRefLang", Version = "0" }; + var myConcept = new DynamicConcept("id-Concept", tinyRefLang) { Key = "key-MyConcept", Name = "MyConcept" }; + tinyRefLang.AddEntities([myConcept]); + + var singularRef = + new DynamicReference("id-MyConcept-singularRef", myConcept) + { + Key = "key-MyConcept-singularRef", Name = "singularRef", Type = myConcept + }; + var multivalueRef = new DynamicReference("id-Concept-multivaluedRef", myConcept) + { + Key = "key-MyConcept-multivaluedRef", Name = "multivaluedRef", Type = myConcept, Multiple = true + }; + myConcept.AddFeatures([singularRef, multivalueRef]); + + TinyRefLang = tinyRefLang; } } \ No newline at end of file diff --git a/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs index 01d9ff3c..7059c6f4 100644 --- a/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs +++ b/src/LionWeb-CSharp/core/Utilities/ReferenceUtils.cs @@ -37,11 +37,15 @@ public record ReferenceValue(IReadableNode SourceNode, Reference Reference, int? /// public static class ReferenceUtils { + /// + /// Returns all values of the given on the given , + /// as s. + /// private static IEnumerable GatherReferenceValues(IReadableNode sourceNode, Reference reference) { if (reference.Multiple) { - var sourceNodes = sourceNode.Get(reference) as IEnumerable ?? []; + IEnumerable sourceNodes = sourceNode.Get(reference) as IEnumerable ?? []; return sourceNodes .Select((targetNode, index) => new ReferenceValue(sourceNode, reference, index, targetNode) @@ -56,6 +60,9 @@ private static IEnumerable GatherReferenceValues(IReadableNode s /// Finds all references within the given , as s. /// To search within all nodes under a collection of root nodes, /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// Note that any reference is found uniquely, + /// i.e. the returned s are pairwise distinct, + /// even if contains duplicate nodes. /// /// The s that are searched for references /// An enumeration of references, as s. @@ -74,6 +81,9 @@ public static IEnumerable ReferenceValues(IEnumerable, as s. /// To search within all nodes under a collection of root nodes, /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// Note that any reference is found uniquely, + /// i.e. the returned s are pairwise distinct, + /// even if or contain duplicate nodes. /// /// The target nodes for which the incoming references are searched /// The s that are searched for references @@ -91,6 +101,9 @@ public static IEnumerable FindIncomingReferences(IEnumerable, as s. /// To search within all nodes under a collection of root nodes, /// pass rootNodes.SelectMany(rootNode => rootNode.Descendants(true, true) as scope. + /// Note that any reference is found uniquely, + /// i.e. the returned s are pairwise distinct, + /// even if contains duplicate nodes. /// /// A target /// The s that form the scope of the search diff --git a/test/LionWeb-CSharp-Test/languages/generated/TinyRefLang.g.cs b/test/LionWeb-CSharp-Test/languages/generated/TinyRefLang.g.cs new file mode 100644 index 00000000..17a8a26d --- /dev/null +++ b/test/LionWeb-CSharp-Test/languages/generated/TinyRefLang.g.cs @@ -0,0 +1,214 @@ +// Generated by the C# M2TypesGenerator: modify at your own risk! +// ReSharper disable InconsistentNaming +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable 1591 +#nullable enable +namespace Examples.TinyRefLang; +using LionWeb.Core; +using LionWeb.Core.M2; +using LionWeb.Core.M3; +using LionWeb.Core.Utilities; +using System; +using System.Collections.Generic; + +[LionCoreLanguage(Key = "key-tinyRefLang", Version = "0")] +public class TinyRefLangLanguage : LanguageBase +{ + public static readonly TinyRefLangLanguage Instance = new Lazy(() => new("id-TinyRefLang")).Value; + public TinyRefLangLanguage(string id) : base(id) + { + _myConcept = new(() => new ConceptBase("id-Concept", this) { Key = "key-MyConcept", Name = "MyConcept", Abstract = false, Partition = false, FeaturesLazy = new(() => [MyConcept_singularRef, MyConcept_multivaluedRef]) }); + _myConcept_singularRef = new(() => new ReferenceBase("id-MyConcept-singularRef", MyConcept, this) { Key = "key-MyConcept-singularRef", Name = "singularRef", Optional = false, Multiple = false, Type = MyConcept }); + _myConcept_multivaluedRef = new(() => new ReferenceBase("id-Concept-multivaluedRef", MyConcept, this) { Key = "key-MyConcept-multivaluedRef", Name = "multivaluedRef", Optional = false, Multiple = true, Type = MyConcept }); + } + + /// + public override IReadOnlyList Entities => [MyConcept]; + /// + public override IReadOnlyList DependsOn => []; + + /// + public override ITinyRefLangFactory GetFactory() => new TinyRefLangFactory(this); + private const string _key = "key-tinyRefLang"; + /// + public override string Key => _key; + + private const string _name = "TinyRefLang"; + /// + public override string Name => _name; + + private const string _version = "0"; + /// + public override string Version => _version; + + private readonly Lazy _myConcept; + public Concept MyConcept => _myConcept.Value; + + private readonly Lazy _myConcept_singularRef; + public Reference MyConcept_singularRef => _myConcept_singularRef.Value; + + private readonly Lazy _myConcept_multivaluedRef; + public Reference MyConcept_multivaluedRef => _myConcept_multivaluedRef.Value; +} + +public interface ITinyRefLangFactory : INodeFactory +{ + public MyConcept NewMyConcept(string id); + public MyConcept CreateMyConcept(); +} + +public class TinyRefLangFactory : AbstractBaseNodeFactory, ITinyRefLangFactory +{ + private readonly TinyRefLangLanguage _language; + public TinyRefLangFactory(TinyRefLangLanguage language) : base(language) + { + _language = language; + } + + /// + public override INode CreateNode(string id, Classifier classifier) + { + if (_language.MyConcept.EqualsIdentity(classifier)) + return NewMyConcept(id); + throw new UnsupportedClassifierException(classifier); + } + + /// + public override Enum GetEnumerationLiteral(EnumerationLiteral literal) + { + throw new UnsupportedEnumerationLiteralException(literal); + } + + public virtual MyConcept NewMyConcept(string id) => new(id); + public virtual MyConcept CreateMyConcept() => NewMyConcept(GetNewId()); +} + +[LionCoreMetaPointer(Language = typeof(TinyRefLangLanguage), Key = "key-MyConcept")] +public class MyConcept : NodeBase +{ + public MyConcept(string id) : base(id) + { + } + + /// + public override Classifier GetClassifier() => TinyRefLangLanguage.Instance.MyConcept; + /// + protected override bool GetInternal(Feature? feature, out Object? result) + { + if (base.GetInternal(feature, out result)) + return true; + if (TinyRefLangLanguage.Instance.MyConcept_singularRef.EqualsIdentity(feature)) + { + result = SingularRef; + return true; + } + + if (TinyRefLangLanguage.Instance.MyConcept_multivaluedRef.EqualsIdentity(feature)) + { + result = MultivaluedRef; + return true; + } + + return false; + } + + /// + protected override bool SetInternal(Feature? feature, Object? value) + { + if (base.SetInternal(feature, value)) + return true; + if (TinyRefLangLanguage.Instance.MyConcept_singularRef.EqualsIdentity(feature)) + { + if (value is Examples.TinyRefLang.MyConcept v) + { + SingularRef = v; + return true; + } + + throw new InvalidValueException(feature, value); + } + + if (TinyRefLangLanguage.Instance.MyConcept_multivaluedRef.EqualsIdentity(feature)) + { + var enumerable = TinyRefLangLanguage.Instance.MyConcept_multivaluedRef.AsNodes(value).ToList(); + AssureNonEmpty(enumerable, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + _multivaluedRef.Clear(); + AddMultivaluedRef(enumerable); + return true; + } + + return false; + } + + /// + public override IEnumerable CollectAllSetFeatures() + { + var result = base.CollectAllSetFeatures().ToList(); + if (_singularRef != default) + result.Add(TinyRefLangLanguage.Instance.MyConcept_singularRef); + if (_multivaluedRef.Count != 0) + result.Add(TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + return result; + } + + private MyConcept? _singularRef = null; + /// Required Single Reference + /// If SingularRef has not been set + /// If set to null + [LionCoreMetaPointer(Language = typeof(TinyRefLangLanguage), Key = "key-MyConcept-singularRef")] + [LionCoreFeature(Kind = LionCoreFeatureKind.Reference, Optional = false, Multiple = false)] + public MyConcept SingularRef { get => _singularRef ?? throw new UnsetFeatureException(TinyRefLangLanguage.Instance.MyConcept_singularRef); set => SetSingularRef(value); } + + /// Required Single Reference + /// If set to null + public MyConcept SetSingularRef(MyConcept value) + { + AssureNotNull(value, TinyRefLangLanguage.Instance.MyConcept_singularRef); + _singularRef = value; + return this; + } + + private readonly List _multivaluedRef = []; + /// Required Multiple Reference + /// If MultivaluedRef is empty + [LionCoreMetaPointer(Language = typeof(TinyRefLangLanguage), Key = "key-MyConcept-multivaluedRef")] + [LionCoreFeature(Kind = LionCoreFeatureKind.Reference, Optional = false, Multiple = false)] + public IReadOnlyList MultivaluedRef { get => AsNonEmptyReadOnly(_multivaluedRef, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); init => AddMultivaluedRef(value); } + + /// Required Multiple Reference + /// If both MultivaluedRef and nodes are empty + public MyConcept AddMultivaluedRef(IEnumerable nodes) + { + var safeNodes = nodes?.ToList(); + AssureNotNull(safeNodes, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + AssureNonEmpty(safeNodes, _multivaluedRef, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + _multivaluedRef.AddRange(safeNodes); + return this; + } + + /// Required Multiple Reference + /// If both MultivaluedRef and nodes are empty + /// If index negative or greater than MultivaluedRef.Count + public MyConcept InsertMultivaluedRef(int index, IEnumerable nodes) + { + AssureInRange(index, _multivaluedRef); + var safeNodes = nodes?.ToList(); + AssureNotNull(safeNodes, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + AssureNonEmpty(safeNodes, _multivaluedRef, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + _multivaluedRef.InsertRange(index, safeNodes); + return this; + } + + /// Required Multiple Reference + /// If MultivaluedRef would be empty + public MyConcept RemoveMultivaluedRef(IEnumerable nodes) + { + var safeNodes = nodes?.ToList(); + AssureNotNull(safeNodes, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + AssureNonEmpty(safeNodes, _multivaluedRef, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + AssureNotClearing(safeNodes, _multivaluedRef, TinyRefLangLanguage.Instance.MyConcept_multivaluedRef); + RemoveAll(safeNodes, _multivaluedRef); + return this; + } +} \ No newline at end of file diff --git a/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs index 33ada421..6cc89fbf 100644 --- a/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs +++ b/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs @@ -23,8 +23,8 @@ namespace LionWeb_CSharp_Test.tests; using Examples.Shapes.Dynamic; using Examples.Shapes.M2; +using Examples.TinyRefLang; using LionWeb.Core; -using LionWeb.Core.M3; using LionWeb.Core.Utilities; /// @@ -82,21 +82,16 @@ public void finds_a_reference_from_an_annotation() [TestMethod] public void finds_a_reference_to_itself() { - var language = new DynamicLanguage("lang"); - var concept = new DynamicConcept("concept", language); - language.AddEntities([concept]); - - var selfRef = new DynamicReference("selfRef", concept); - concept.AddFeatures([selfRef]); - selfRef.Type = concept; + var language = TinyRefLangLanguage.Instance; + var factory = language.GetFactory(); - var node = new DynamicNode("node", concept); - node.Set(selfRef, node); + var node = factory.CreateMyConcept(); + node.SingularRef = node; CollectionAssert.AreEqual( new List { - new (node, selfRef, null, node) + new (node, language.MyConcept_singularRef, null, node) }, ReferenceUtils.FindIncomingReferences(node, [node]).ToList() ); @@ -105,28 +100,19 @@ public void finds_a_reference_to_itself() [TestMethod] public void finds_references_in_different_features_of_the_source() { - var language = new DynamicLanguage("lang"); - var concept = new DynamicConcept("concept", language); - language.AddEntities([concept]); - - var ref1 = new DynamicReference("ref1", concept); - concept.AddFeatures([ref1]); - ref1.Type = concept; - - var ref2 = new DynamicReference("ref2", concept); - concept.AddFeatures([ref2]); - ref2.Type = concept; + var language = TinyRefLangLanguage.Instance; + var factory = language.GetFactory(); - var targetNode = new DynamicNode("targetNode", concept); - var sourceNode = new DynamicNode("sourceNode", concept); - sourceNode.Set(ref1, targetNode); - sourceNode.Set(ref2, targetNode); + var targetNode = factory.CreateMyConcept(); + var sourceNode = factory.CreateMyConcept(); + sourceNode.SingularRef = targetNode; + sourceNode.AddMultivaluedRef([targetNode]); CollectionAssert.AreEquivalent( new List { - new (sourceNode, ref1, null, targetNode), - new (sourceNode, ref2, null, targetNode), + new (sourceNode, language.MyConcept_singularRef, null, targetNode), + new (sourceNode, language.MyConcept_multivaluedRef, 0, targetNode), }, ReferenceUtils.FindIncomingReferences(targetNode, [sourceNode]).ToList() ); @@ -135,24 +121,18 @@ public void finds_references_in_different_features_of_the_source() [TestMethod] public void finds_multiple_references_to_target_in_a_multivalued_feature_of_the_source() { - var language = new DynamicLanguage("lang"); - var concept = new DynamicConcept("concept", language); - language.AddEntities([concept]); - - var myRef = new DynamicReference("myRef", concept); - concept.AddFeatures([myRef]); - myRef.Type = concept; - myRef.Multiple = true; + var language = TinyRefLangLanguage.Instance; + var factory = language.GetFactory(); - var targetNode = new DynamicNode("targetNode", concept); - var sourceNode = new DynamicNode("sourceNode", concept); - sourceNode.Set(myRef, new List { targetNode, targetNode }); + var targetNode = factory.CreateMyConcept(); + var sourceNode = factory.CreateMyConcept(); + sourceNode.AddMultivaluedRef([targetNode, targetNode]); CollectionAssert.AreEquivalent( new List { - new (sourceNode, myRef, 0, targetNode), - new (sourceNode, myRef, 1, targetNode), + new (sourceNode, language.MyConcept_multivaluedRef, 0, targetNode), + new (sourceNode, language.MyConcept_multivaluedRef, 1, targetNode), }, ReferenceUtils.FindIncomingReferences(targetNode, [sourceNode]).ToList() ); @@ -161,26 +141,21 @@ public void finds_multiple_references_to_target_in_a_multivalued_feature_of_the_ [TestMethod] public void finds_references_among_multiple_sources_and_targets() { - var language = new DynamicLanguage("lang"); - var concept = new DynamicConcept("concept", language); - language.AddEntities([concept]); - - var myRef = new DynamicReference("myRef", concept); - concept.AddFeatures([myRef]); - myRef.Type = concept; + var language = TinyRefLangLanguage.Instance; + var factory = language.GetFactory(); - var sourceNode1 = new DynamicNode("sourceNode1", concept); - var sourceNode2 = new DynamicNode("sourceNode2", concept); - var targetNode1 = new DynamicNode("targetNode1", concept); - var targetNode2 = new DynamicNode("targetNode2", concept); - sourceNode1.Set(myRef, targetNode1); - sourceNode2.Set(myRef, targetNode2); + var sourceNode1 = factory.CreateMyConcept(); + var sourceNode2 = factory.CreateMyConcept(); + var targetNode1 = factory.CreateMyConcept(); + var targetNode2 = factory.CreateMyConcept(); + sourceNode1.SingularRef = targetNode1; + sourceNode2.SingularRef = targetNode2; CollectionAssert.AreEquivalent( new List { - new (sourceNode1, myRef, null, targetNode1), - new (sourceNode2, myRef, null, targetNode2), + new (sourceNode1, language.MyConcept_singularRef, null, targetNode1), + new (sourceNode2, language.MyConcept_singularRef, null, targetNode2), }, ReferenceUtils.FindIncomingReferences([targetNode1, targetNode2], [sourceNode1, sourceNode2]).ToList() );