Skip to content

Commit

Permalink
Adds support for element/relationship property expressions (closes #297
Browse files Browse the repository at this point in the history
…).
  • Loading branch information
simonbrowndotje committed Jun 27, 2024
1 parent b4211a9 commit d455e14
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 47 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ subprojects { proj ->

description = 'Structurizr'
group = 'com.structurizr'
version = '2.1.4'
version = '2.2.0'

repositories {
mavenCentral()
Expand Down
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 2.2.0 (unreleased)

- structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297).

## 2.1.4 (18th June 2024)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ public boolean hasTag(String tag) {
return getTagsAsSet().contains(tag.trim());
}

/**
* Determines whether this model item has the given property with the given value.
*
* @param name the name of the property
* @param value the value of the property
* @return true if the named property is present with the given value, false otherwise
*/
public boolean hasProperty(String name, String value) {
return getProperties().containsKey(name) && getProperties().get(name).equals(value);
}

/**
* Gets the URL where more information about this item can be found.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ static boolean isExpression(String token) {
token.startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) ||
token.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION) ||
token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) ||
token.startsWith(ELEMENT_EQUALS_EXPRESSION) ||
token.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) ||
token.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION) ||
token.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.toLowerCase()) ||
token.startsWith(RELATIONSHIP_EQUALS_EXPRESSION);
Expand Down Expand Up @@ -169,6 +171,15 @@ private Set<ModelItem> evaluateExpression(String expr, DslContext context) {
modelItems.add(element);
}
});
} else if (expr.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION)) {
String propertyName = expr.substring(expr.indexOf("[")+1, expr.indexOf("]"));
String propertyValue = expr.substring(expr.indexOf("==")+2);

context.getWorkspace().getModel().getElements().forEach(element -> {
if (hasProperty(element, propertyName, propertyValue)) {
modelItems.add(element);
}
});
} else if (expr.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION)) {
String[] tags = expr.substring(RELATIONSHIP_TAG_EQUALS_EXPRESSION.length()).split(",");
context.getWorkspace().getModel().getRelationships().forEach(relationship -> {
Expand All @@ -183,6 +194,15 @@ private Set<ModelItem> evaluateExpression(String expr, DslContext context) {
modelItems.add(relationship);
}
});
} else if (expr.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION)) {
String propertyName = expr.substring(expr.indexOf("[")+1, expr.indexOf("]"));
String propertyValue = expr.substring(expr.indexOf("==")+2);

context.getWorkspace().getModel().getRelationships().forEach(relationship -> {
if (hasProperty(relationship, propertyName, propertyValue)) {
modelItems.add(relationship);
}
});
} else if (expr.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION)) {
String identifier = expr.substring(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.length());
Set<Element> sourceElements = new HashSet<>();
Expand Down Expand Up @@ -276,6 +296,28 @@ private boolean hasAllTags(ModelItem modelItem, String[] tags) {
return result;
}

private boolean hasProperty(ModelItem modelItem, String name, String value) {
boolean result = modelItem.hasProperty(name, value);

if (!result) {
// perhaps the property is instead on a related model item?
if (modelItem instanceof StaticStructureElementInstance) {
StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)modelItem;
result = elementInstance.getElement().hasProperty(name, value);
} else if (modelItem instanceof Relationship) {
Relationship relationship = (Relationship)modelItem;
if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) {
Relationship linkedRelationship = relationship.getModel().getRelationship(relationship.getLinkedRelationshipId());
if (linkedRelationship != null) {
result = linkedRelationship.hasProperty(name, value);
}
}
}
}

return result;
}

protected abstract Set<Element> findAfferentCouplings(Element element);

protected <T extends Element> Set<Element> findAfferentCouplings(Element element, Class<T> typeOfElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ class StructurizrDslExpressions {
static final String ELEMENT_TYPE_EQUALS_EXPRESSION = "element.type==";
static final String ELEMENT_TAG_EQUALS_EXPRESSION = "element.tag==";
static final String ELEMENT_TAG_NOT_EQUALS_EXPRESSION = "element.tag!=";
static final String ELEMENT_PROPERTY_EQUALS_EXPRESSION = "element\\.properties\\[.*]==.*";

static final String ELEMENT_EQUALS_EXPRESSION = "element==";
static final String ELEMENT_PARENT_EQUALS_EXPRESSION = "element.parent==";

static final String RELATIONSHIP_TAG_EQUALS_EXPRESSION = "relationship.tag==";
static final String RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION = "relationship.tag!=";
static final String RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION = "relationship\\.properties\\[.*]==.*";

static final String RELATIONSHIP_SOURCE_EQUALS_EXPRESSION = "relationship.source==";
static final String RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION = "relationship.destination==";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

class AbstractExpressionParserTests extends AbstractTests {

private StaticViewExpressionParser parser = new StaticViewExpressionParser();
private final StaticViewExpressionParser parser = new StaticViewExpressionParser();

@Test
void test_parseExpression_ThrowsAnException_WhenTheRelationshipSourceIsSpecifiedUsingLongSyntaxButDoesNotExist() {
Expand Down Expand Up @@ -385,35 +385,36 @@ void test_parseExpression_ReturnsAllRelationships_WhenUsingTheWildcardRelationsh
}

@Test
void test_parseExpression_ReturnsElements_WhenUsingAnElementTagExpression() {
void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementTagEqualsExpression() {
model.addPerson("User");
SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System");

SystemLandscapeView view = views.createSystemLandscapeView("key", "Description");
SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view);
context.setWorkspace(workspace);
context.setIdentifierRegister(new IdentifiersRegister());
SoftwareSystem ss = model.addSoftwareSystem("Software System");
SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss);

Set<ModelItem> elements = parser.parseExpression("element.tag==Software System", context);
assertEquals(1, elements.size());
assertTrue(elements.contains(softwareSystem));
Set<ModelItem> elements = parser.parseExpression("element.tag==Software System", context());
assertEquals(2, elements.size());
assertTrue(elements.contains(ss)); // this is tagged "Software System"
assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is
}

@Test
void test_parseExpression_ReturnsElementInstances_WhenUsingAnElementTagExpression() {
model.addPerson("User");
SoftwareSystem ss = model.addSoftwareSystem("Software System");
SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss);
void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementPropertyEqualsExpression() {
SoftwareSystem a = model.addSoftwareSystem("A");
a.addProperty("Technical Debt", "Low");
SoftwareSystem b = model.addSoftwareSystem("B");
b.addProperty("Technical Debt", "Medium");
SoftwareSystem c = model.addSoftwareSystem("C");
c.addProperty("Technical Debt", "High");
SoftwareSystem d = model.addSoftwareSystem("D");

DeploymentView view = views.createDeploymentView("key", "Description");
DeploymentViewDslContext context = new DeploymentViewDslContext(view);
context.setWorkspace(workspace);
context.setIdentifierRegister(new IdentifiersRegister());
DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node");
SoftwareSystemInstance ai = deploymentNode.add(a);
SoftwareSystemInstance bi = deploymentNode.add(b);
SoftwareSystemInstance ci = deploymentNode.add(c);

Set<ModelItem> elements = parser.parseExpression("element.tag==Software System", context);
Set<ModelItem> elements = parser.parseExpression("element.properties[Technical Debt]==High", context());
assertEquals(2, elements.size());
assertTrue(elements.contains(ss)); // this is tagged "Software System"
assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is
assertTrue(elements.contains(c)); // this has the property
assertTrue(elements.contains(ci)); // this doesn't have the property, but the element it's based upon does
}

@Test
Expand Down Expand Up @@ -465,42 +466,53 @@ void test_parseExpression_ReturnsElements_WhenUsingAnElementParentExpression() {
}

@Test
void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpression() {
void test_parseExpression_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipTagEqualsExpression() {
Person user = model.addPerson("User");
SoftwareSystem a = model.addSoftwareSystem("A");
Container aa = a.addContainer("AA");
SoftwareSystem b = model.addSoftwareSystem("B");
Relationship r = a.uses(b, "Uses");
r.addTags("Tag 1");
Container bb = b.addContainer("BB");
SoftwareSystem c = model.addSoftwareSystem("C");
Container cc = c.addContainer("CC");

SystemLandscapeView view = views.createSystemLandscapeView("key", "Description");
SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view);
context.setWorkspace(workspace);
context.setIdentifierRegister(new IdentifiersRegister());
Relationship r1 = user.uses(aa, "Uses");
r1.addTags("Tag 1");
Relationship r2 = user.uses(bb, "Uses");
r2.addTags("Tag 2");
Relationship r3 = user.uses(cc, "Uses");

Set<ModelItem> relationships = parser.parseExpression("relationship.tag==Tag 1", context);
assertEquals(1, relationships.size());
assertTrue(relationships.contains(r));
Set<ModelItem> relationships = parser.parseExpression("relationship.tag==Tag 1", context());
assertEquals(2, relationships.size());
assertTrue(relationships.contains(r1));

Relationship impliedRelationship = user.getEfferentRelationshipWith(a);
assertTrue(relationships.contains(impliedRelationship));
}

@Test
void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpressionAndTheTagIsSetOnTheLinkedRelationship() {
void test_parseExpression_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipPropertyEqualsExpression() {
Person user = model.addPerson("User");
SoftwareSystem a = model.addSoftwareSystem("A");
Container aa = a.addContainer("AA");
SoftwareSystem b = model.addSoftwareSystem("B");
Relationship r = a.uses(b, "Uses");
r.addTags("Tag 1");
Container bb = b.addContainer("BB");
SoftwareSystem c = model.addSoftwareSystem("C");
Container cc = c.addContainer("CC");

DeploymentNode dn = model.addDeploymentNode("DN");
SoftwareSystemInstance ai = dn.add(a);
SoftwareSystemInstance bi = dn.add(b);
Relationship r1 = user.uses(aa, "Uses");
r1.addProperty("Secure", "Yes");
Relationship r2 = user.uses(bb, "Uses");
r2.addProperty("Secure", "No");
Relationship r3 = user.uses(cc, "Uses");

DeploymentView view = views.createDeploymentView("key", "Description");
DeploymentViewDslContext context = new DeploymentViewDslContext(view);
context.setWorkspace(workspace);
context.setIdentifierRegister(new IdentifiersRegister());
assertEquals(6, model.getRelationships().size());

Set<ModelItem> relationships = parser.parseExpression("relationship.tag==Tag 1", context);
Set<ModelItem> relationships = parser.parseExpression("relationship.properties[Secure]==Yes", context());
assertEquals(2, relationships.size());
assertTrue(relationships.contains(r));
assertTrue(relationships.contains(ai.getRelationships().iterator().next()));
assertTrue(relationships.contains(r1));

Relationship impliedRelationship = user.getEfferentRelationshipWith(a);
assertTrue(relationships.contains(impliedRelationship));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ abstract class AbstractTests {
protected Model model = workspace.getModel();
protected ViewSet views = workspace.getViews();

protected ModelDslContext context() {
AbstractTests() {
model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy());
}

protected ModelDslContext context() {
ModelDslContext context = new ModelDslContext();
context.setWorkspace(workspace);

Expand Down

0 comments on commit d455e14

Please sign in to comment.