From ace67d6f6dde60ddae0cfb68c6653bac7e0a4837 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Sun, 11 Oct 2026 16:31:18 +0200 Subject: [PATCH] [Add] Property query extension methods and their Handlebars helpers; refs #511 --- .../PropertyExtensionsTestFixture.cs | 87 +++++++++++++++ uml4net.Extensions/PropertyExtensions.cs | 104 ++++++++++++++++++ .../PropertyHelperTestFixture.cs | 55 +++++++++ uml4net.HandleBars/PropertyHelper.cs | 48 ++++++++ 4 files changed, 294 insertions(+) diff --git a/uml4net.Extensions.Tests/PropertyExtensionsTestFixture.cs b/uml4net.Extensions.Tests/PropertyExtensionsTestFixture.cs index bbed62c33..2302b1a13 100644 --- a/uml4net.Extensions.Tests/PropertyExtensionsTestFixture.cs +++ b/uml4net.Extensions.Tests/PropertyExtensionsTestFixture.cs @@ -29,6 +29,7 @@ namespace uml4net.Extensions.Tests using Serilog; + using uml4net.CommonStructure; using uml4net.Packages; using uml4net.SimpleClassifiers; using uml4net.StructuredClassifiers; @@ -798,5 +799,91 @@ public void Verify_that_QueryHasBeenRedefined_returns_expected_result() Assert.That(result, Is.False); } + + [Test] + public void Verify_that_QueryIsEnumPropertyWithDefaultValue_returns_expected_result() + { + var root = this.xmiReaderResult.QueryRoot(xmiId: "_0", name: "UML"); + + var commonStructurePackage = root.NestedPackage.Single(x => x.Name == "CommonStructure"); + + var packageableElement = commonStructurePackage.PackagedElement.OfType().Single(x => x.Name == "PackageableElement"); + var namedElement = commonStructurePackage.PackagedElement.OfType().Single(x => x.Name == "NamedElement"); + + var visibilityWithDefault = packageableElement.OwnedAttribute.Single(x => x.Name == "visibility"); + var visibilityWithoutDefault = namedElement.OwnedAttribute.Single(x => x.Name == "visibility"); + var name = namedElement.OwnedAttribute.Single(x => x.Name == "name"); + + var enumeration = new Enumeration { Name = "Colour" }; + var withLiteralStringAsDefault = new Property { Name = "colour", Type = enumeration }; + withLiteralStringAsDefault.DefaultValue.Add(new LiteralString { Value = "red" }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(visibilityWithDefault.QueryIsEnumPropertyWithDefaultValue(), Is.True, "the default is the literal public"); + Assert.That(visibilityWithoutDefault.QueryIsEnumPropertyWithDefaultValue(), Is.False, "an enumeration without default"); + Assert.That(name.QueryIsEnumPropertyWithDefaultValue(), Is.False, "not typed by an enumeration"); + Assert.That(withLiteralStringAsDefault.QueryIsEnumPropertyWithDefaultValue(), Is.False, "the default is not an InstanceValue"); + Assert.That(() => PropertyExtensions.QueryIsEnumPropertyWithDefaultValue(null), Throws.ArgumentNullException); + } + } + + [Test] + public void Verify_that_QueryIsPartOfNonDerivedCompositeAggregation_returns_expected_result() + { + var root = this.xmiReaderResult.QueryRoot(xmiId: "_0", name: "UML"); + + var commonStructurePackage = root.NestedPackage.Single(x => x.Name == "CommonStructure"); + var activitiesPackage = root.NestedPackage.Single(x => x.Name == "Activities"); + + var element = commonStructurePackage.PackagedElement.OfType().Single(x => x.Name == "Element"); + var comment = commonStructurePackage.PackagedElement.OfType().Single(x => x.Name == "Comment"); + var activity = activitiesPackage.PackagedElement.OfType().Single(x => x.Name == "Activity"); + var activityNode = activitiesPackage.PackagedElement.OfType().Single(x => x.Name == "ActivityNode"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(element.OwnedAttribute.Single(x => x.Name == "ownedComment").QueryIsPartOfNonDerivedCompositeAggregation(), Is.True, "composite and not derived"); + Assert.That(element.OwnedAttribute.Single(x => x.Name == "ownedElement").QueryIsPartOfNonDerivedCompositeAggregation(), Is.False, "composite and derived"); + Assert.That(activity.OwnedAttribute.Single(x => x.Name == "node").QueryIsPartOfNonDerivedCompositeAggregation(), Is.True, "the end that refers to the contained elements"); + Assert.That(activityNode.OwnedAttribute.Single(x => x.Name == "activity").QueryIsPartOfNonDerivedCompositeAggregation(), Is.True, "the end that refers to the container"); + Assert.That(comment.OwnedAttribute.Single(x => x.Name == "annotatedElement").QueryIsPartOfNonDerivedCompositeAggregation(), Is.False, "a reference that is not part of a composition"); + Assert.That(comment.OwnedAttribute.Single(x => x.Name == "body").QueryIsPartOfNonDerivedCompositeAggregation(), Is.False); + Assert.That(() => PropertyExtensions.QueryIsPartOfNonDerivedCompositeAggregation(null), Throws.ArgumentNullException); + } + } + + [Test] + public void Verify_that_QueryConstraints_returns_the_rules_of_the_owner_that_constrain_the_property() + { + var @class = new Class { Name = "Tank" }; + + var volume = new Property { Name = "volume" }; + var mass = new Property { Name = "mass" }; + @class.OwnedAttribute.Add(volume); + @class.OwnedAttribute.Add(mass); + + var volumeIsPositive = new Constraint { Name = "volumeIsPositive" }; + volumeIsPositive.ConstrainedElement.Add(volume); + + var volumeAndMass = new Constraint { Name = "volumeAndMass" }; + volumeAndMass.ConstrainedElement.Add(mass); + volumeAndMass.ConstrainedElement.Add(volume); + + var classRule = new Constraint { Name = "classRule" }; + classRule.ConstrainedElement.Add(@class); + + @class.OwnedRule.Add(volumeIsPositive); + @class.OwnedRule.Add(classRule); + @class.OwnedRule.Add(volumeAndMass); + + using (Assert.EnterMultipleScope()) + { + Assert.That(volume.QueryConstraints(), Is.EqualTo(new[] { volumeIsPositive, volumeAndMass }), "in the order of the owned rules"); + Assert.That(mass.QueryConstraints(), Is.EqualTo(new[] { volumeAndMass })); + Assert.That(new Property { Name = "detached" }.QueryConstraints(), Is.Empty, "a property without owner"); + Assert.That(() => PropertyExtensions.QueryConstraints(null), Throws.ArgumentNullException); + } + } } } diff --git a/uml4net.Extensions/PropertyExtensions.cs b/uml4net.Extensions/PropertyExtensions.cs index 641985054..a995a6c3d 100644 --- a/uml4net.Extensions/PropertyExtensions.cs +++ b/uml4net.Extensions/PropertyExtensions.cs @@ -22,6 +22,7 @@ namespace uml4net.Extensions { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; using uml4net.Classification; using uml4net.CommonStructure; @@ -572,5 +573,108 @@ public static bool QueryIsMemberOfManyToMany(this IProperty property) return false; } + + /// + /// Queries whether the is typed by an and has one of the + /// literals of an enumeration as default value + /// + /// + /// The subject + /// + /// + /// true when the type of the is an and its default + /// value is an that refers to an , false + /// otherwise + /// + /// + /// UML 2.5.1 specifies the default value of a property that is typed by an enumeration as an InstanceValue + /// that refers to the EnumerationLiteral. A default value of another kind, such as a LiteralString that + /// holds the name of the literal, is not taken into account. + /// + public static bool QueryIsEnumPropertyWithDefaultValue(this IProperty property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (!property.QueryIsEnum()) + { + return false; + } + + return property.DefaultValue.FirstOrDefault() is IInstanceValue instanceValue + && instanceValue.Instance is IEnumerationLiteral; + } + + /// + /// Queries whether the is one of the two ends of a composite aggregation that is not + /// derived + /// + /// + /// The subject + /// + /// + /// true when the is composite and not derived (the end that refers to the + /// contained elements), or when its is composite and not derived (the end + /// that refers to the container); false otherwise + /// + /// + /// only has a value when both ends of the association are owned by the + /// classifiers at its ends. The end that refers to the container is therefore not recognized when the + /// composite end, or that end itself, is owned by the association. + /// + public static bool QueryIsPartOfNonDerivedCompositeAggregation(this IProperty property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (property.IsComposite && !property.IsDerived) + { + return true; + } + + var opposite = property.Opposite; + + return opposite != null && opposite.IsComposite && !opposite.IsDerived; + } + + /// + /// Queries the constraints that constrain the : the rules that are owned by the + /// namespace that owns the and that have the as one of + /// their constrained elements + /// + /// + /// The subject + /// + /// + /// The s of Namespace::ownedRule of the owner of the + /// for which Constraint::constrainedElement holds the + /// , in the order of the owned rules. An empty collection when the owner of the + /// is not an . + /// + /// + /// A constraint that concerns the but has another element, such as the owning + /// class, as constrained element is not returned. + /// + public static ReadOnlyCollection QueryConstraints(this IProperty property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (!(property.Owner is INamespace owner)) + { + return new List().AsReadOnly(); + } + + return owner.OwnedRule + .Where(x => x.ConstrainedElement.Contains(property)) + .ToList() + .AsReadOnly(); + } } } diff --git a/uml4net.HandleBars.Tests/PropertyHelperTestFixture.cs b/uml4net.HandleBars.Tests/PropertyHelperTestFixture.cs index dbf91cd77..6942a5536 100644 --- a/uml4net.HandleBars.Tests/PropertyHelperTestFixture.cs +++ b/uml4net.HandleBars.Tests/PropertyHelperTestFixture.cs @@ -617,6 +617,61 @@ public void Verify_that_NameEqualsEnclosingType_helper_only_writes_the_block_whe } } + [Test] + public void Verify_that_QueryIsEnumPropertyWithDefaultValue_helper_returns_expected_result() + { + var template = this.handlebarsContext.Compile("{{#if (#Property.QueryIsEnumPropertyWithDefaultValue this)}}enum with default{{else}}other{{/if}}"); + + var visibilityWithDefault = this.QueryClass("CommonStructure", "PackageableElement").OwnedAttribute.Single(x => x.Name == "visibility"); + var visibilityWithoutDefault = this.QueryClass("CommonStructure", "NamedElement").OwnedAttribute.Single(x => x.Name == "visibility"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(template(visibilityWithDefault), Is.EqualTo("enum with default")); + Assert.That(template(visibilityWithoutDefault), Is.EqualTo("other")); + Assert.That(() => template(this.QueryClass("CommonStructure", "NamedElement")), Throws.TypeOf()); + Assert.That(() => this.handlebarsContext.Compile("{{#if (#Property.QueryIsEnumPropertyWithDefaultValue this this)}}x{{/if}}")(visibilityWithDefault), Throws.TypeOf()); + } + } + + [Test] + public void Verify_that_QueryIsPartOfNonDerivedCompositeAggregation_helper_returns_expected_result() + { + var template = this.handlebarsContext.Compile("{{#if (#Property.QueryIsPartOfNonDerivedCompositeAggregation this)}}composition{{else}}other{{/if}}"); + + var element = this.QueryClass("CommonStructure", "Element"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(template(element.OwnedAttribute.Single(x => x.Name == "ownedComment")), Is.EqualTo("composition")); + Assert.That(template(element.OwnedAttribute.Single(x => x.Name == "ownedElement")), Is.EqualTo("other")); + Assert.That(() => template(element), Throws.TypeOf()); + Assert.That(() => this.handlebarsContext.Compile("{{#if (#Property.QueryIsPartOfNonDerivedCompositeAggregation this this)}}x{{/if}}")(element), Throws.TypeOf()); + } + } + + [Test] + public void Verify_that_QueryConstraints_helper_returns_expected_result() + { + var template = this.handlebarsContext.Compile("{{#each (#Property.QueryConstraints this) as | constraint |}}{{ constraint.Name }};{{/each}}"); + + var @class = new Class { Name = "Tank" }; + var volume = new Property { Name = "volume" }; + @class.OwnedAttribute.Add(volume); + + var volumeIsPositive = new Constraint { Name = "volumeIsPositive" }; + volumeIsPositive.ConstrainedElement.Add(volume); + @class.OwnedRule.Add(volumeIsPositive); + @class.OwnedRule.Add(new Constraint { Name = "classRule" }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(template(volume), Is.EqualTo("volumeIsPositive;")); + Assert.That(() => template(@class), Throws.TypeOf()); + Assert.That(() => this.handlebarsContext.Compile("{{#each (#Property.QueryConstraints this this)}}x{{/each}}")(volume), Throws.TypeOf()); + } + } + /// private IClass QueryClass(string packageName, string className) { diff --git a/uml4net.HandleBars/PropertyHelper.cs b/uml4net.HandleBars/PropertyHelper.cs index c7d884970..cb2b5592f 100644 --- a/uml4net.HandleBars/PropertyHelper.cs +++ b/uml4net.HandleBars/PropertyHelper.cs @@ -325,6 +325,54 @@ public static void RegisterPropertyHelper(this IHandlebars handlebars) return property.QueryIsSubsetted(); }); + // Queries whether the Property is typed by an Enumeration and has an EnumerationLiteral as default value + handlebars.RegisterHelper("Property.QueryIsEnumPropertyWithDefaultValue", (_, arguments) => + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{#Property.QueryIsEnumPropertyWithDefaultValue}} helper must have exactly one argument"); + } + + if (!(arguments[0] is IProperty property)) + { + throw new ArgumentException("{{#Property.QueryIsEnumPropertyWithDefaultValue}} - supposed to be IProperty"); + } + + return property.QueryIsEnumPropertyWithDefaultValue(); + }); + + // Queries whether the Property is one of the two ends of a composite aggregation that is not derived + handlebars.RegisterHelper("Property.QueryIsPartOfNonDerivedCompositeAggregation", (_, arguments) => + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{#Property.QueryIsPartOfNonDerivedCompositeAggregation}} helper must have exactly one argument"); + } + + if (!(arguments[0] is IProperty property)) + { + throw new ArgumentException("{{#Property.QueryIsPartOfNonDerivedCompositeAggregation}} - supposed to be IProperty"); + } + + return property.QueryIsPartOfNonDerivedCompositeAggregation(); + }); + + // Queries the constraints of the owner of the Property that have the Property as constrained element + handlebars.RegisterHelper("Property.QueryConstraints", (_, arguments) => + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{#Property.QueryConstraints}} helper must have exactly one argument"); + } + + if (!(arguments[0] is IProperty property)) + { + throw new ArgumentException("{{#Property.QueryConstraints}} - supposed to be IProperty"); + } + + return property.QueryConstraints(); + }); + // Queries whether the Property is Derived, DerivedUnion or ReadOnly handlebars.RegisterHelper("Property.QueryIsDerivedOrDerivedUnionOrReadOnly", (_, arguments) => {