Skip to content

Commit

Permalink
Adds a couple more supporting types strategies.
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbrowndotje committed Sep 26, 2024
1 parent e6a78c7 commit d5e0d20
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 2 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## 3.1.0 (unreleased)

- structurizr-client: Workspace archive file now includes the branch name in the filename.
- structurizr-component: Adds `ImplementationWithPrefixSupportingTypesStrategy`.
- structurizr-component: Adds `ImplementationWithSuffixSupportingTypesStrategy`.
- structurizr-dsl: Adds `supportingTypes implementation-prefix <prefix>`.
- structurizr-dsl: Adds `supportingTypes implementation-suffix <suffix>`.

## 3.0.0 (19th September 2024)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.structurizr.util.StringUtils;
import org.apache.bcel.classfile.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.*;

Expand All @@ -10,6 +12,8 @@
*/
public class Type {

private static final Log log = LogFactory.getLog(Type.class);

private static final String STRUCTURIZR_TAG_ANNOTATION = "Lcom/structurizr/annotation/Tag;";
private static final String STRUCTURIZR_TAGS_ANNOTATION = "Lcom/structurizr/annotation/Tags;";

Expand Down Expand Up @@ -92,6 +96,10 @@ public boolean isAbstractClass() {
return javaClass.isAbstract() && javaClass.isClass();
}

public boolean isInterface() {
return javaClass.isInterface();
}

public List<String> getTags() {
List<String> tags = new ArrayList<>();

Expand Down Expand Up @@ -159,4 +167,16 @@ public String toString() {
return this.fullyQualifiedName;
}

public boolean implementsInterface(Type type) {
if (javaClass != null) {
try {
return javaClass.implementationOf(type.javaClass);
} catch (ClassNotFoundException e) {
log.warn(e);
}
}

return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.structurizr.component.supporting;

import com.structurizr.component.Type;
import com.structurizr.component.TypeRepository;

import java.util.Set;
import java.util.stream.Collectors;

/**
* A strategy that, given an interface, finds the implementation class with the specified prefix.
*/
public class ImplementationWithPrefixSupportingTypesStrategy implements SupportingTypesStrategy {

private final String prefix;

public ImplementationWithPrefixSupportingTypesStrategy(String prefix) {
this.prefix = prefix;
}

@Override
public Set<Type> findSupportingTypes(Type type, TypeRepository typeRepository) {
if (!type.isInterface()) {
throw new IllegalArgumentException("The type " + type.getFullyQualifiedName() + " is not an interface");
}

return typeRepository.getTypes().stream()
.filter(dependency -> dependency.implementsInterface(type) && dependency.getName().equals(prefix + type.getName()))
.collect(Collectors.toSet());
}

@Override
public String toString() {
return "ImplementationWithPrefixSupportingTypesStrategy{" +
"prefix='" + prefix + '\'' +
'}';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.structurizr.component.supporting;

import com.structurizr.component.Type;
import com.structurizr.component.TypeRepository;

import java.util.Set;
import java.util.stream.Collectors;

/**
* A strategy that, given an interface, finds the implementation class with the specified suffix.
*/
public class ImplementationWithSuffixSupportingTypesStrategy implements SupportingTypesStrategy {

private final String suffix;

public ImplementationWithSuffixSupportingTypesStrategy(String suffix) {
this.suffix = suffix;
}

@Override
public Set<Type> findSupportingTypes(Type type, TypeRepository typeRepository) {
if (!type.isInterface()) {
throw new IllegalArgumentException("The type " + type.getFullyQualifiedName() + " is not an interface");
}

return typeRepository.getTypes().stream()
.filter(dependency -> dependency.implementsInterface(type) && dependency.getName().equals(type.getName() + suffix))
.collect(Collectors.toSet());
}

@Override
public String toString() {
return "ImplementationWithSuffixSupportingTypesStrategy{" +
"suffix='" + suffix + '\'' +
'}';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.structurizr.component.supporting;

import com.structurizr.component.Type;
import com.structurizr.component.TypeRepository;
import org.apache.bcel.classfile.ClassParser;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

public class ImplementationWithPrefixSupportingTypesStrategyTests {

private final File classes = new File("build/classes/java/test");

@Test
void findSupportingTypes() throws Exception {
Type interfaceType = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepository.class").getAbsolutePath()).parse());
Type implementationTypeWithPrefix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse());
Type implementationTypeWithSuffix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.class").getAbsolutePath()).parse());

TypeRepository typeRepository = new TypeRepository();
typeRepository.add(interfaceType);
typeRepository.add(implementationTypeWithPrefix);
typeRepository.add(implementationTypeWithSuffix);

Set<Type> supportingTypes = new ImplementationWithPrefixSupportingTypesStrategy("Jdbc").findSupportingTypes(interfaceType, typeRepository);
assertEquals(1, supportingTypes.size());
assertTrue(supportingTypes.contains(implementationTypeWithPrefix));
}

@Test
void findSupportingTypes_ThrowsAnException_WhenTheTypeIsNotAnInterface() throws Exception {
try {
Type type = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse());
new ImplementationWithPrefixSupportingTypesStrategy("Impl").findSupportingTypes(type, null);
fail();
} catch (Exception e) {
assertEquals("The type com.structurizr.component.supporting.implementation.JdbcExampleRepository is not an interface", e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.structurizr.component.supporting;

import com.structurizr.component.Type;
import com.structurizr.component.TypeRepository;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

public class ImplementationWithSuffixSupportingTypesStrategyTests {

private final File classes = new File("build/classes/java/test");

@Test
void findSupportingTypes() throws Exception {
Type interfaceType = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepository.class").getAbsolutePath()).parse());
Type implementationTypeWithPrefix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse());
Type implementationTypeWithSuffix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.class").getAbsolutePath()).parse());

TypeRepository typeRepository = new TypeRepository();
typeRepository.add(interfaceType);
typeRepository.add(implementationTypeWithPrefix);
typeRepository.add(implementationTypeWithSuffix);

Set<Type> supportingTypes = new ImplementationWithSuffixSupportingTypesStrategy("Impl").findSupportingTypes(interfaceType, typeRepository);
assertEquals(1, supportingTypes.size());
assertTrue(supportingTypes.contains(implementationTypeWithSuffix));
}

@Test
void findSupportingTypes_ThrowsAnException_WhenTheTypeIsNotAnInterface() throws Exception {
try {
Type type = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse());
new ImplementationWithSuffixSupportingTypesStrategy("Impl").findSupportingTypes(type, null);
fail();
} catch (Exception e) {
assertEquals("The type com.structurizr.component.supporting.implementation.JdbcExampleRepository is not an interface", e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.structurizr.component.supporting.implementation;

public interface ExampleRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.structurizr.component.supporting.implementation;

public class ExampleRepositoryImpl implements ExampleRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.structurizr.component.supporting.implementation;

public class JdbcExampleRepository implements ExampleRepository {
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ final class ComponentFinderStrategyParser extends AbstractParser {
private static final String SUPPORTING_TYPES_REFERENCED_IN_PACKAGE = "referenced-in-package";
private static final String SUPPORTING_TYPES_IN_PACKAGE = "in-package";
private static final String SUPPORTING_TYPES_UNDER_PACKAGE = "under-package";
private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX = "implementation-prefix";
private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX = "implementation-suffix";
private static final String SUPPORTING_TYPES_NONE = "none";
private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_NONE)) + "> [parameters]";
private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX, SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX, SUPPORTING_TYPES_NONE)) + "> [parameters]";
private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR = "supportingTypes implementation-prefix <prefix>";
private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR = "supportingTypes implementation-suffix <suffix>";

private static final String NAME_TYPE_NAME = "type-name";
private static final String NAME_FQN = "fqn";
Expand Down Expand Up @@ -185,6 +189,22 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke
case SUPPORTING_TYPES_UNDER_PACKAGE:
context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy());
break;
case SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX:
if (tokens.size() < 3) {
throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR);
}

String prefix = tokens.get(2);
context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithPrefixSupportingTypesStrategy(prefix));
break;
case SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX:
if (tokens.size() < 3) {
throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR);
}

String suffix = tokens.get(2);
context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithSuffixSupportingTypesStrategy(suffix));
break;
case SUPPORTING_TYPES_NONE:
context.getComponentFinderStrategyBuilder().supportedBy(new DefaultSupportingTypesStrategy());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,42 @@ void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() {
parser.parseSupportingTypes(context, tokens("supportingTypes"), null);
fail();
} catch (Exception e) {
assertEquals("Too few tokens, expected: supportingTypes <all-referenced|referenced-in-package|in-package|under-package|none> [parameters]", e.getMessage());
assertEquals("Too few tokens, expected: supportingTypes <all-referenced|referenced-in-package|in-package|under-package|implementation-prefix|implementation-suffix|none> [parameters]", e.getMessage());
}
}

@Test
void test_parseSupportingTypes_ThrowsAnException_WhenImplementationSuffixIsUsedWithoutASuffix() {
try {
parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix"), null);
fail();
} catch (Exception e) {
assertEquals("Too few tokens, expected: supportingTypes implementation-suffix <suffix>", e.getMessage());
}
}

@Test
void test_parseSupportingTypes_ImplementationSuffix() {
parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix", "Impl"), null);
assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithSuffixSupportingTypesStrategy{suffix='Impl'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString());
}

@Test
void test_parseSupportingTypes_ThrowsAnException_WhenImplementationPrefixIsUsedWithoutAPrefix() {
try {
parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix"), null);
fail();
} catch (Exception e) {
assertEquals("Too few tokens, expected: supportingTypes implementation-prefix <prefix>", e.getMessage());
}
}

@Test
void test_parseSupportingTypes_ImplementationPrefix() {
parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix", "Jdbc"), null);
assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithPrefixSupportingTypesStrategy{prefix='Jdbc'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString());
}

@Test
void test_parseName_ThrowsAnException_WhenNoTypeIsSpecified() {
try {
Expand Down

0 comments on commit d5e0d20

Please sign in to comment.