diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 655f39d0a..05d91635e 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -111,6 +111,7 @@ All validation rules have been updated to comply with the SysMLv2 2025-04 specif === New features - https://github.com/eclipse-syson/syson/issues/1396[#1396] [general-view] Add a graphical edge representation for `IncludeUseCaseUsage` in the _General View_ diagram. +- https://github.com/eclipse-syson/syson/issues/1405[#1405] [export] Implement the textual export of `FlowUsage`. == v2025.6.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 03e303ad7..e8062c871 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 @@ -103,6 +103,108 @@ public void tearDown() { assertThat(payload).isInstanceOf(SuccessPayload.class); } + @Test + @DisplayName("GIVEN a model with basic FlowUsages, WHEN importing/exporting the file, THEN the exported text file should be the same as the imported one.") + public void checkFlowUsageBaseExample() throws IOException { + var input = """ + part def P1Def { + port po1 : PortDef1; + } + port def PortDef1 { + out item item1 : P2Def; + } + part def P2Def; + part def P3Def { + in item item2 : P3Def; + } + part p1 { + part p2 : P1Def; + part p3 : P3Def; + flow from p2.po1.item1 to p3.item2; + flow f1 from p2.po1.item1 to p3.item2; + }"""; + this.checker.check(input, input); + } + + @Test + @DisplayName("GIVEN a model with FlowUsages with payload, WHEN importing/exporting the file, THEN the exported text file should be semantically the same as the imported one.") + public void checkFlowUsageWithPayload() throws IOException { + var input = """ + package 'Port Example' { + attribute def Temp; + part def Fuel; + port def FuelOutPort { + attribute temperature : Temp; + out item fuelSupply : Fuel; + in item fuelReturn : Fuel; + } + port def FuelInPort { + attribute temperature : Temp; + in item fuelSupply : Fuel; + out item fuelReturn : Fuel; + } + part def FuelTankAssembly { + port fuelTankPort : FuelOutPort; + } + part def Engine { + port engineFuelPort : FuelInPort; + } + } + package 'Flow Connection Interface Example' { + private import 'Port Example'::*; + part def Vehicle; + part vehicle : Vehicle { + part tankAssy : FuelTankAssembly; + part eng : Engine; + flow of Fuel from tankAssy.fuelTankPort.fuelSupply to eng.engineFuelPort.fuelSupply; + flow of Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of [1] Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of Fuel [1] from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of fuel : Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of fuel [1] : Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + } + }"""; + // Expected difference : Multiplicity are always after Type + var expected = """ + package 'Port Example' { + attribute def Temp; + part def Fuel; + port def FuelOutPort { + attribute temperature : Temp; + out item fuelSupply : Fuel; + in item fuelReturn : Fuel; + } + port def FuelInPort { + attribute temperature : Temp; + in item fuelSupply : Fuel; + out item fuelReturn : Fuel; + } + part def FuelTankAssembly { + port fuelTankPort : FuelOutPort; + } + part def Engine { + port engineFuelPort : FuelInPort; + } + } + package 'Flow Connection Interface Example' { + private import 'Port Example'::*; + part def Vehicle; + part vehicle : Vehicle { + part tankAssy : FuelTankAssembly; + part eng : Engine; + flow of Fuel from tankAssy.fuelTankPort.fuelSupply to eng.engineFuelPort.fuelSupply; + flow of Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of Fuel [1] from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of Fuel [1] from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of fuel : Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of fuel : Fuel [1] from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + flow of Fuel from eng.engineFuelPort.fuelReturn to tankAssy.fuelTankPort.fuelReturn; + } + }"""; + this.checker.check(input, expected); + } + @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 ca2d12188..388e0158a 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 @@ -26,6 +26,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; @@ -344,6 +345,16 @@ public String caseFeatureReferenceExpression(FeatureReferenceExpression expressi return builder.toString(); } + @Override + public String caseFlowUsage(FlowUsage flowConnectionUsage) { + var builder = this.newAppender(); + this.appendOccurrenceUsagePrefix(builder, flowConnectionUsage); + builder.appendWithSpaceIfNeeded("flow"); + this.appendFlowDeclaration(builder, flowConnectionUsage); + this.appendDefinitionBody(builder, flowConnectionUsage); + return builder.toString(); + } + @Override public String caseForkNode(ForkNode forkNode) { Appender builder = this.newAppender(); @@ -2268,4 +2279,80 @@ private void appendArgumentExpression(Appender builder, Expression argument) { } } } + + private void appendFlowDeclaration(Appender builder, FlowUsage flowConnectionUsage) { + this.appendUsageDeclaration(builder, flowConnectionUsage); + EList ends = flowConnectionUsage.getFlowEnd(); + Optional payload = flowConnectionUsage.getFeatureMembership().stream() + .map(FeatureMembership::getOwnedMemberFeature) + .filter(PayloadFeature.class::isInstance) + .map(PayloadFeature.class::cast) + .findFirst(); + if (payload.isPresent()) { + PayloadFeature payloadFeature = payload.get(); + builder.appendWithSpaceIfNeeded("of"); + Appender nameBuilder = this.newAppender(); + this.appendNameWithShortName(nameBuilder, payloadFeature); + + if (nameBuilder.isEmpty()) { + // If no identification part, switch to simple format: + // "ownedRelationship += OwnedFeatureTyping (ownedRelationship += OwnedMultiplicity )?" + this.appendOwnedFeatureTyping(builder, payloadFeature); + this.appendMultiplicityPart(builder, payloadFeature); + } else { + // Handle simple form "Identification? PayloadFeatureSpecializationPart ValuePart?" for the moment + builder.appendWithSpaceIfNeeded(nameBuilder.toString()); + this.appendFeatureSpecilizationPart(builder, payloadFeature, false); + FeatureValue value = this.getValuation(payloadFeature); + if (value != null) { + this.appendValuePart(builder, flowConnectionUsage); + } + } + } + + if (!ends.isEmpty()) { + builder.appendWithSpaceIfNeeded("from"); + FlowEnd sourceEnd = ends.get(0); + Feature sourceFeature = flowConnectionUsage.getSourceOutputFeature(); + this.appendFlowEndSubsetting(builder, sourceEnd,sourceFeature); + } + + if (ends.size() > 1) { + builder.appendWithSpaceIfNeeded("to"); + FlowEnd targetEnd = ends.get(1); + Feature targetFeature = flowConnectionUsage.getTargetInputFeature(); + this.appendFlowEndSubsetting(builder, targetEnd,targetFeature); + } + } + + private void appendOwnedFeatureTyping(Appender builder, PayloadFeature payloadFeature) { + String types = payloadFeature.getOwnedTyping().stream() + .map(FeatureTyping::getType) + .filter(Objects::nonNull) + .map(t -> { + if (t instanceof Feature feature) { + return this.appendFeatureRefOrFeatureChain(feature, payloadFeature); + } else { + return this.getDeresolvableName(t, payloadFeature); + } + }).collect(joining(",")); + builder.appendWithSpaceIfNeeded(types); + } + + private void appendFlowEndSubsetting(Appender builder, FlowEnd sourceEnd, Feature referencedFeature) { + ReferenceSubsetting refSubSetting = sourceEnd.getOwnedReferenceSubsetting(); + Feature subFeature = refSubSetting.getSubsettedFeature(); + builder.appendWithSpaceIfNeeded(this.appendFeatureRefOrFeatureChain(subFeature, sourceEnd)); + builder.append(".").append(this.getDeresolvableName(referencedFeature, sourceEnd)); + } + + private String appendFeatureRefOrFeatureChain(Feature feature, Element context) { + Appender builder = this.newAppender(); + if (feature.getChainingFeature().isEmpty()) { + builder.appendWithSpaceIfNeeded(this.getDeresolvableName(feature, context)); + } else { + this.appendFeatureChain(builder, feature); + } + return builder.toString(); + } } diff --git a/doc/content/modules/user-manual/pages/release-notes/2025.8.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2025.8.0.adoc index a3f73e5ee..a6bcc3d0f 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2025.8.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2025.8.0.adoc @@ -19,6 +19,27 @@ All existing models/projects in {product} will be automatically migrated to this - Add a new edge tool from `UseCaseUsage` to create `IncludeUseCaseUsage` in the _General view_ diagram. image::gv-IncludeUseCaseUsage.png[Manage Visibility modal, width=65%,height=65%] +- Implement the textual export of `FlowUsage`. +The following model now properly export the `FlowUsage` elements. + +``` +part def P1Def { + port po1 : PortDef1; +} +port def PortDef1 { + out item item1 : P2Def; +} +part def P2Def; +part def P3Def { + in item item2 : P3Def; +} +part p1 { + part p2 : P1Def; + part p3 : P3Def; + flow from p2.po1.item1 to p3.item2; + flow f1 from p2.po1.item1 to p3.item2; +} +``` == Bug fixes