Skip to content

Commit

Permalink
Initial version of DSL wrapper for the component finder.
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbrowndotje committed Aug 20, 2024
1 parent 477bac9 commit 0f2b05f
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
## unreleased

- structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316).
- structurizr-component: Initial rewrite of the original `structurizr-analysis` library - provides a way to automatically find components in a Java codebase.
- structurizr-dsl: Adds name-value properties to dynamic view relationship views.
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM).
- structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views.
- structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java").
- structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression.
- structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression.
- structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder.

## 2.2.0 (2nd July 2024)

Expand Down
1 change: 1 addition & 0 deletions structurizr-dsl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {

api project(':structurizr-client')
api project(':structurizr-import')
api project(':structurizr-component')

testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.19'
testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.structurizr.dsl;

import com.structurizr.component.ComponentFinderBuilder;
import com.structurizr.model.Container;

final class ComponentFinderDslContext extends DslContext {

private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder();

ComponentFinderDslContext(Container container) {
componentFinderBuilder.forContainer(container);
}

@Override
protected String[] getPermittedTokens() {
return new String[] {
StructurizrDslTokens.COMPONENT_FINDER_CLASSES_TOKEN,
StructurizrDslTokens.COMPONENT_FINDER_SOURCE_TOKEN,
StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_TOKEN
};
}

ComponentFinderBuilder getComponentFinderBuilder() {
return this.componentFinderBuilder;
}

@Override
void end() {
componentFinderBuilder.build().findComponents();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.structurizr.dsl;

final class ComponentFinderParser extends AbstractParser {

private static final String CLASSES_GRAMMAR = "classes <path>";
private static final String SOURCE_GRAMMAR = "source <path>";

void parseClasses(ComponentFinderDslContext context, Tokens tokens) {
// classes <path>

if (tokens.hasMoreThan(1)) {
throw new RuntimeException("Too many tokens, expected: " + CLASSES_GRAMMAR);
}

context.getComponentFinderBuilder().fromClasses(tokens.get(1));
}

void parseSource(ComponentFinderDslContext context, Tokens tokens) {
// source <path>

if (tokens.hasMoreThan(1)) {
throw new RuntimeException("Too many tokens, expected: " + SOURCE_GRAMMAR);
}

context.getComponentFinderBuilder().fromSource(tokens.get(1));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.structurizr.dsl;

import com.structurizr.component.ComponentFinderBuilder;
import com.structurizr.component.ComponentFinderStrategyBuilder;

final class ComponentFinderStrategyDslContext extends DslContext {

private final ComponentFinderBuilder componentFinderBuilder;
private final ComponentFinderStrategyBuilder componentFinderStrategyBuilder = new ComponentFinderStrategyBuilder();

ComponentFinderStrategyDslContext(ComponentFinderBuilder componentFinderBuilder) {
this.componentFinderBuilder = componentFinderBuilder;
}

@Override
protected String[] getPermittedTokens() {
return new String[] {
StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN,
StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FILTER_TOKEN,
StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN,
StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAMING_TOKEN
};
}

ComponentFinderStrategyBuilder getComponentFinderStrategyBuilder() {
return this.componentFinderStrategyBuilder;
}

@Override
void end() {
componentFinderBuilder.withStrategy(componentFinderStrategyBuilder.build());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.structurizr.dsl;

import com.structurizr.component.filter.ExcludeTypesByRegexFilter;
import com.structurizr.component.filter.IncludeTypesByRegexFilter;
import com.structurizr.component.matcher.*;
import com.structurizr.component.naming.DefaultPackageNamingStrategy;
import com.structurizr.component.naming.SimpleNamingStrategy;
import com.structurizr.component.naming.FullyQualifiedNamingStrategy;
import com.structurizr.component.supporting.AllReferencedTypesInPackageSupportingTypesStrategy;
import com.structurizr.component.supporting.AllReferencedTypesSupportingTypesStrategy;
import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy;
import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy;

final class ComponentFinderStrategyParser extends AbstractParser {

private static final String MATCHER_GRAMMAR = "matcher <annotation|extends|implements|namesuffix|regex> [parameters]";
private static final String FILTER_GRAMMAR = "filter <includeregex|excluderegex> [parameters]";
private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <referenced|referencedinpackage|inpackage|underpackage> [parameters]";
private static final String NAMING_GRAMMAR = "naming <name|fqn|package>";

void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens) {
if (tokens.size() < 2) {
throw new RuntimeException("Too few tokens, expected: " + MATCHER_GRAMMAR);
}

String type = tokens.get(1).toLowerCase();
switch (type) {
case "annotation":
if (tokens.size() == 4) {
String name = tokens.get(2);
String technology = tokens.get(3);

context.getComponentFinderStrategyBuilder().matchedBy(new AnnotationTypeMatcher(name, technology));
} else {
throw new RuntimeException("Expected: " + MATCHER_GRAMMAR);
}
break;
case "extends":
if (tokens.size() == 4) {
String name = tokens.get(2);
String technology = tokens.get(3);

context.getComponentFinderStrategyBuilder().matchedBy(new ExtendsTypeMatcher(name, technology));
} else {
throw new RuntimeException("Expected: " + MATCHER_GRAMMAR);
}
break;
case "implements":
if (tokens.size() == 4) {
String name = tokens.get(2);
String technology = tokens.get(3);

context.getComponentFinderStrategyBuilder().matchedBy(new ImplementsTypeMatcher(name, technology));
} else {
throw new RuntimeException("Expected: " + MATCHER_GRAMMAR);
}
break;
case "namesuffix":
if (tokens.size() == 4) {
String suffix = tokens.get(2);
String technology = tokens.get(3);

context.getComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher(suffix, technology));
} else {
throw new RuntimeException("Expected: " + MATCHER_GRAMMAR);
}
break;
case "regex":
if (tokens.size() == 4) {
String regex = tokens.get(2);
String technology = tokens.get(3);

context.getComponentFinderStrategyBuilder().matchedBy(new RegexTypeMatcher(regex, technology));
} else {
throw new RuntimeException("Expected: " + MATCHER_GRAMMAR);
}
break;
default:
throw new IllegalArgumentException("Unknown matcher: " + type);
}
}

void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens) {
if (tokens.size() < 2) {
throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR);
}

String type = tokens.get(1).toLowerCase();
switch (type) {
case "includeregex":
if (tokens.size() == 3) {
String regex = tokens.get(2);

context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex));
} else {
throw new RuntimeException("Expected: " + FILTER_GRAMMAR);
}
break;
case "excluderegex":
if (tokens.size() == 3) {
String regex = tokens.get(2);

context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex));
} else {
throw new RuntimeException("Expected: " + FILTER_GRAMMAR);
}
break;
default:
throw new IllegalArgumentException("Unknown filter: " + type);
}
}

void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens tokens) {
if (tokens.size() < 2) {
throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_GRAMMAR);
}

String type = tokens.get(1).toLowerCase();
switch (type) {
case "referenced":
context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesSupportingTypesStrategy());
break;
case "referencedinpackage":
context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesInPackageSupportingTypesStrategy());
break;
case "inpackage":
context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesInPackageSupportingTypesStrategy());
break;
case "underpackage":
context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy());
break;
default:
throw new IllegalArgumentException("Unknown supporting types strategy: " + type);
}
}

void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens) {
if (tokens.size() < 1) {
throw new RuntimeException("Too few tokens, expected: " + NAMING_GRAMMAR);
}

String type = tokens.get(1).toLowerCase();
switch (type) {
case "name":
context.getComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy());
break;
case "fqn":
context.getComponentFinderStrategyBuilder().namedBy(new FullyQualifiedNamingStrategy());
break;
case "package":
context.getComponentFinderStrategyBuilder().namedBy(new DefaultPackageNamingStrategy());
break;
default:
throw new IllegalArgumentException("Unknown naming strategy: " + type);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,36 @@ void parse(List<String> lines, File dslFile, boolean include) throws Structurizr

registerIdentifier(identifier, component);

} else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) {
if (!restricted) {
if (shouldStartContext(tokens)) {
startContext(new ComponentFinderDslContext(getContext(ContainerDslContext.class).getContainer()));
}
}

} else if (COMPONENT_FINDER_CLASSES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) {
new ComponentFinderParser().parseClasses(getContext(ComponentFinderDslContext.class), tokens);

} else if (COMPONENT_FINDER_SOURCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) {
new ComponentFinderParser().parseSource(getContext(ComponentFinderDslContext.class), tokens);

} else if (COMPONENT_FINDER_STRATEGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) {
if (shouldStartContext(tokens)) {
startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder()));
}

} else if (COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) {
new ComponentFinderStrategyParser().parseMatcher(getContext(ComponentFinderStrategyDslContext.class), tokens);

} else if (COMPONENT_FINDER_STRATEGY_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) {
new ComponentFinderStrategyParser().parseFilter(getContext(ComponentFinderStrategyDslContext.class), tokens);

} else if (COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) {
new ComponentFinderStrategyParser().parseSupportingTypes(getContext(ComponentFinderStrategyDslContext.class), tokens);

} else if (COMPONENT_FINDER_STRATEGY_NAMING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) {
new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens);

} else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) {
throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,13 @@ class StructurizrDslTokens {
static final String PLUGIN_TOKEN = "!plugin";
static final String SCRIPT_TOKEN = "!script";

static final String COMPONENT_FINDER_TOKEN = "!components";
static final String COMPONENT_FINDER_CLASSES_TOKEN = "classes";
static final String COMPONENT_FINDER_SOURCE_TOKEN = "source";
static final String COMPONENT_FINDER_STRATEGY_TOKEN = "strategy";
static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher";
static final String COMPONENT_FINDER_STRATEGY_FILTER_TOKEN = "filter";
static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes";
static final String COMPONENT_FINDER_STRATEGY_NAMING_TOKEN = "naming";

}
56 changes: 56 additions & 0 deletions structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,62 @@ void test_Var_CannotOverrideConst() {
}
}

@Test
void springPetClinic() throws Exception {
File path = new File("/Users/simon/sandbox/spring-petclinic");
if (path.exists()) {
System.out.println("Running Spring PetClinic example...");
StructurizrDslParser parser = new StructurizrDslParser();
parser.addConstant("SPRING_PETCLINIC_DIR", path.getAbsolutePath());
parser.parse(new File("src/test/resources/dsl/spring-petclinic.dsl"));

Container webApplication = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.webApplication");
assertEquals(7, webApplication.getComponents().size());

Component welcomeController = webApplication.getComponentWithName("Welcome Controller");
assertNotNull(welcomeController);
assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src"));

Component ownerController = webApplication.getComponentWithName("Owner Controller");
assertNotNull(ownerController);
assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src"));

Component petController = webApplication.getComponentWithName("Pet Controller");
assertNotNull(petController);
assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src"));

Component vetController = webApplication.getComponentWithName("Vet Controller");
assertNotNull(vetController);
assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src"));

Component visitController = webApplication.getComponentWithName("Visit Controller");
assertNotNull(visitController);
assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src"));

Component ownerRepository = webApplication.getComponentWithName("Owner Repository");
assertNotNull(ownerRepository);
assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src"));

Component vetRepository = webApplication.getComponentWithName("Vet Repository");
assertNotNull(vetRepository);
assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type"));
assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src"));

assertTrue(welcomeController.getRelationships().isEmpty());

assertNotNull(petController.getEfferentRelationshipWith(ownerRepository));
assertNotNull(visitController.getEfferentRelationshipWith(ownerRepository));
assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository));

assertNotNull(vetController.getEfferentRelationshipWith(vetRepository));
}
}

@Test
void test_bulkOperations() throws Exception {
Expand Down
Loading

0 comments on commit 0f2b05f

Please sign in to comment.