Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -2268,4 +2279,80 @@ private void appendArgumentExpression(Appender builder, Expression argument) {
}
}
}

private void appendFlowDeclaration(Appender builder, FlowUsage flowConnectionUsage) {
this.appendUsageDeclaration(builder, flowConnectionUsage);
EList<FlowEnd> ends = flowConnectionUsage.getFlowEnd();
Optional<PayloadFeature> 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();
}
}
21 changes: 21 additions & 0 deletions doc/content/modules/user-manual/pages/release-notes/2025.8.0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading