From 72ae680b668cfa99242477f34f2dd0156d6fc84a Mon Sep 17 00:00:00 2001 From: Arthur Daussy Date: Tue, 30 Dec 2025 14:07:50 +0100 Subject: [PATCH] [1786] Implement textual export of RequirementConstraintMembership Bug: https://github.com/eclipse-syson/syson/issues/1786 Signed-off-by: Arthur Daussy --- CHANGELOG.adoc | 1 + .../application/export/ImportExportTests.java | 67 +++++++++++++++ .../sysml/textual/SysMLElementSerializer.java | 82 +++++++++++++++++-- .../textual/utils/SysMLKeywordSwitch.java | 6 ++ .../pages/release-notes/2026.1.0.adoc | 30 +++++++ 5 files changed, 181 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c3a9d4873..97eb88da9 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -51,6 +51,7 @@ A new compartment named _satisfy requirements_ has also been added to `PartDefin - https://github.com/eclipse-syson/syson/issues/1737[#1737] [diagrams] Add creation tools to InterconnectionCompartmentNode - https://github.com/eclipse-syson/syson/issues/1395[#1395] Provide a way to duplicate a semantic element ans its representation node in the _General View_ diagram. - https://github.com/eclipse-syson/syson/issues/1740[#1740] [diagrams] Add _items_ compartment and graphical border node on `PortDefinition` graphical nodes in the _General View_ diagram. +- https://github.com/eclipse-syson/syson/issues/1786[#1786] [export] Implement textual export of `RequirementConstraintMembership`. == v2025.12.0 diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java index adc89d526..749be6add 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java @@ -216,6 +216,73 @@ public void checkFlowUsageWithPayload() throws IOException { this.checker.check(input, expected); } + @Test + @DisplayName("GIVEN a model with RequirementConstraintMembership, WHEN importing/exporting the file, THEN the exported text file should be the same as the imported one using the full notation.") + public void checkRequirementConstraintMembershipFullNotation() throws IOException { + var input = """ + private import SI::kilogram; + private import ScalarValues::*; + part def P1 { + attribute x : Real; + } + requirement r1 { + subject sub : P1; + attribute actualMass :> ISQBase::mass; + attribute maxMass :> ISQBase::mass; + assume constraint NamedRequirementConstraint { + actualMass <= maxMass + } + assume constraint { + sub.x <= 500 + } + require constraint { + actualMass >= 500 [kg] + } + } + constraint def C; + constraint c : C; + requirement def R1 { + require constraint c1 :>> c; + }"""; + this.checker.check(input, input); + } + + @Test + @DisplayName("GIVEN a model with RequirementConstraintMembership having ReferenceSubsetting, WHEN importing/exporting the file, THEN the exported text file should be the same as the imported one" + + "using the shorthand notation when possible.") + public void checkRequirementConstraintMembershipShorthandNotation() throws IOException { + var input = """ + requirement r1; + requirement r3 { + requirement r4 { + requirement r5; + } + require r1; + require r4.r5; + require r4 { + doc /* Some doc */ + } + }"""; + this.checker.check(input, input); + } + + @Test + @DisplayName("GIVEN a model with RequirementConstraintMembership having MetadataUsage, WHEN importing/exporting the file, THEN the exported text file should be the same as the imported one.") + public void checkRequirementConstraintMembershipWithMetadataUsage() throws IOException { + var input = """ + private import Metaobjects::SemanticMetadata; + requirement def Goal; + requirement goals : Goal; + metadata def goal :> SemanticMetadata { + :>> baseType = goals meta SysML::Systems::RequirementUsage; + } + #goal requirement r2 { + assume #goal constraint c1; + require #goal constraint c2; + }"""; + this.checker.check(input, input); + } + @Test @DisplayName("GIVEN a model with ForkNode, WHEN importing/exporting the file, THEN the exported text file should be the same as the imported one.") public void checkForkNode() throws IOException { diff --git a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java index 8be936bd5..f31f22ad1 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java +++ b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java @@ -121,6 +121,7 @@ import org.eclipse.syson.sysml.ReferenceUsage; import org.eclipse.syson.sysml.Relationship; import org.eclipse.syson.sysml.RenderingUsage; +import org.eclipse.syson.sysml.RequirementConstraintMembership; import org.eclipse.syson.sysml.RequirementDefinition; import org.eclipse.syson.sysml.RequirementUsage; import org.eclipse.syson.sysml.ReturnParameterMembership; @@ -925,6 +926,24 @@ public String caseRenderingUsage(RenderingUsage rendering) { return builder.toString(); } + @Override + public String caseRequirementConstraintMembership(RequirementConstraintMembership requirementConstraintMembership) { + var builder = this.newAppender(); + this.appendMembershipPrefix(requirementConstraintMembership, builder); + String kind = switch (requirementConstraintMembership.getKind()) { + case REQUIREMENT -> "require"; + case ASSUMPTION -> "assume"; + }; + builder.appendWithSpaceIfNeeded(kind); + requirementConstraintMembership.getOwnedRelatedElement().stream() + .filter(ConstraintUsage.class::isInstance) + .map(ConstraintUsage.class::cast) + .forEach(constraintUsage -> this.appendRequirementConstraintUsage(builder, constraintUsage)); + + + return builder.toString(); + } + @Override public String caseRequirementDefinition(RequirementDefinition requirement) { Appender builder = this.newAppender(); @@ -2461,15 +2480,29 @@ private void appendExtensionKeyword(Appender builder, Type type) { owningMember.getOwnedRelatedElement().stream() .filter(MetadataUsage.class::isInstance) .map(MetadataUsage.class::cast) - .map(MetadataUsage::getMetadataDefinition) - .filter(Objects::nonNull) - .forEach(mDef -> this.appendPrefixMetadataMember(builder, mDef, type)); + .forEach(metadataUsage -> this.appendPrefixMetadataMember(builder, metadataUsage, type)); } } } - private void appendPrefixMetadataMember(Appender builder, Metaclass def, Type type) { - builder.appendSpaceIfNeeded().append("#").append(this.getDeresolvableName(def, type)); + private void appendPrefixMetadataMember(Appender builder, MetadataUsage metadataUsage, Type type) { + Metaclass def = metadataUsage.getMetadataDefinition(); + if (def != null) { + builder.appendSpaceIfNeeded().append("#").append(this.getDeresolvableName(def, type)); + this.childrenMembershipToSkip.add(metadataUsage.getOwningMembership()); + } + + } + + private void appendRequirementConstraintUsage(Appender builder, ConstraintUsage constraintUsage) { + if (this.useRequirementConstraintUsageShortHandNotation(constraintUsage)) { + this.appendRequirementConstraintUsageShorthandNotation(builder, constraintUsage); + } else { + // Only use the full form : + // UsageExtensionKeyword* 'constraint' ConstraintUsageDeclaration CalculationBody + builder.appendWithSpaceIfNeeded(this.caseConstraintUsage(constraintUsage)); + } + } private void appendSimpleName(Appender appender, Element e) { @@ -2706,4 +2739,43 @@ private String appendFeatureRefOrFeatureChain(Feature feature, Element context) } return builder.toString(); } + + private void appendRequirementConstraintUsageShorthandNotation(Appender builder, ConstraintUsage constraintUsage) { + ReferenceSubsetting ownedReferenceSubsetting = constraintUsage.getOwnedReferenceSubsetting(); + if (ownedReferenceSubsetting != null) { + this.appendOwnedReferenceSubsetting(builder, ownedReferenceSubsetting); + } + Appender metadataUsageBuilder = this.newAppender(); + this.appendExtensionKeyword(metadataUsageBuilder, constraintUsage); + List ownedSpecialization = constraintUsage.getOwnedSpecialization().stream() + // The owned reference subsetting is already handled previously + .filter(specialization -> specialization != ownedReferenceSubsetting) + .toList(); + this.appendFeatureSpecializationPart(builder, constraintUsage, ownedSpecialization, false); + this.appendChildrenContent(builder, constraintUsage, constraintUsage.getOwnedMembership()); + } + + /** + * Checks if the shorthand notation for a {@link ConstraintUsage} stored in a {@link RequirementConstraintMembership} can be used. + * Shorthand format :"ownedRelationship += OwnedReferenceSubsetting FeatureSpecializationPart? RequirementBody" + *

+ * Use this form when : + *

    + *
  • has one referenceSubsetting
  • + *
  • has no declared name
  • + *
  • has no declared shortName
  • + *
  • has no MetadataUsage
  • + *
+ *

+ * + * @param constraintUsage + * the constraint to test + * @return {@code true} if the shorthand notation can be used + */ + private boolean useRequirementConstraintUsageShortHandNotation(ConstraintUsage constraintUsage) { + return constraintUsage.getDeclaredName() == null + && constraintUsage.getDeclaredShortName() == null + && constraintUsage.getOwnedReferenceSubsetting() != null + && constraintUsage.getNestedMetadata().isEmpty(); + } } diff --git a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/utils/SysMLKeywordSwitch.java b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/utils/SysMLKeywordSwitch.java index f71efbd54..279e5fcd6 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/utils/SysMLKeywordSwitch.java +++ b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/utils/SysMLKeywordSwitch.java @@ -22,6 +22,7 @@ import org.eclipse.syson.sysml.AttributeUsage; import org.eclipse.syson.sysml.ConcernDefinition; import org.eclipse.syson.sysml.ConcernUsage; +import org.eclipse.syson.sysml.ConstraintDefinition; import org.eclipse.syson.sysml.ConstraintUsage; import org.eclipse.syson.sysml.EnumerationDefinition; import org.eclipse.syson.sysml.EnumerationUsage; @@ -129,6 +130,11 @@ public String caseActionUsage(ActionUsage object) { return ACTION; } + @Override + public String caseConstraintDefinition(ConstraintDefinition object) { + return CONSTRAINT_KEYWORD; + } + @Override public String caseEnumerationUsage(EnumerationUsage object) { if (object.getOwningDefinition() instanceof EnumerationDefinition && !this.isNullOrEmpty(object.getName())) { diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc index ab26b4d3c..efab8ae2f 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc @@ -18,6 +18,36 @@ image::explorer-duplicate-object-dialog.png[Duplicate Object dialog, width=25%,h image::manage-elements-duplicate-from-diagram.png[Duplicate element from Diagram] +- `RequirementConstraintMembership` are now properly exported in the textual format such as in: + +``` +private import SI::kilogram; +private import ScalarValues::*; +part def P1 { + attribute x : Real; +} +requirement r1 { + subject sub : P1; + attribute actualMass :> ISQBase::mass; + attribute maxMass :> ISQBase::mass; + assume constraint NamedRequirementConstraint { //Here + actualMass <= maxMass + } + assume constraint { // And here + sub.x <= 500 + } + require constraint { // And here + actualMass >= 500 [kg] + } +} +constraint def C; +constraint c : C; +requirement def R1 { + require constraint c1 :>> c; // And here +} +``` + + == Bug fixes - Fix an issue that displayed imported libraries at the root of the project if they contained `LibraryPackage` elements and non-`Namespace` elements.