Skip to content

Commit 9cccac6

Browse files
author
Stefan Kapferer
committed
First PoC
1 parent b5a8b15 commit 9cccac6

24 files changed

+622
-2
lines changed

build.gradle

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ repositories {
1616
}
1717

1818
dependencies {
19-
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
20-
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
19+
implementation "com.tngtech.archunit:archunit-junit5:${archUnitVersion}"
20+
implementation "org.contextmapper:context-mapper-dsl:${cmlVersion}"
21+
implementation "org.jmolecules:jmolecules-ddd:${jmoleculesVersion}"
22+
implementation "org.jmolecules:jmolecules-events:${jmoleculesVersion}"
23+
implementation "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}"
24+
25+
testImplementation "org.assertj:assertj-core:${assertJVersion}"
26+
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${jUnitVersion}"
2127
}
2228

29+
sourceSets.test.java.srcDirs = ['src/test/java','src/test/cml']
30+
2331
test {
2432
useJUnitPlatform()
2533
}

gradle.properties

+6
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ ossSnapshotRepository=https://oss.sonatype.org/content/repositories/snapshots/
33
ossReleaseStagingRepository=https://oss.sonatype.org/service/local/staging/deploy/maven2/
44

55
# dependency versions
6+
jUnitVersion=5.7.0
7+
assertJVersion=3.19.0
8+
archUnitVersion=0.18.0
9+
cmlVersion=6.5.0
10+
jmoleculesVersion=1.2.0
11+
xtendLibVersion=2.19.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.contextmapper.archunit;
2+
3+
import com.tngtech.archunit.core.domain.JavaClasses;
4+
import com.tngtech.archunit.core.importer.ClassFileImporter;
5+
import com.tngtech.archunit.core.importer.ImportOption;
6+
import org.contextmapper.archunit.cml.BoundedContextResolver;
7+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import static org.contextmapper.archunit.ContextMapperArchRules.*;
12+
13+
/**
14+
* An abstract test class that can be extended in order to execute all our ArchRules for a single Bounded Context.
15+
*/
16+
public abstract class AbstractTacticArchUnitTest {
17+
18+
protected BoundedContext context;
19+
protected JavaClasses classes;
20+
21+
/**
22+
* Implement this method to define the Bounded Context name.
23+
*/
24+
protected abstract String getBoundedContextName();
25+
26+
/**
27+
* Implement this method to define against which CML file you want to test.
28+
*/
29+
protected abstract String getCMLFilePath();
30+
31+
/**
32+
* Implement this method to defined the java package within which you want to test.
33+
*/
34+
protected abstract String getJavaPackageName2Test();
35+
36+
@BeforeEach
37+
protected void setup() {
38+
this.context = new BoundedContextResolver()
39+
.resolveBoundedContextFromModel(getCMLFilePath(), getBoundedContextName());
40+
this.classes = importClasses();
41+
}
42+
43+
/**
44+
* Override this method to change class import behavior.
45+
*/
46+
protected JavaClasses importClasses() {
47+
return new ClassFileImporter()
48+
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
49+
.importPackages(getJavaPackageName2Test());
50+
}
51+
52+
@Test
53+
void aggregatesShouldBeModeledInCML() {
54+
aggregateClassesShouldBeModeledInCml(context).check(classes);
55+
}
56+
57+
@Test
58+
void entitiesShouldBeModeledInCML() {
59+
entityClassesShouldBeModeledInCml(context).check(classes);
60+
}
61+
62+
@Test
63+
void valueObjectsShouldBeModeledInCML() {
64+
valueObjectClassesShouldBeModeledInCml(context).check(classes);
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.contextmapper.archunit;
2+
3+
import org.contextmapper.archunit.conditions.ModeledAsAggregateInContext;
4+
import org.contextmapper.archunit.conditions.ModeledAsEntityInContext;
5+
import org.contextmapper.archunit.conditions.ModeledAsValueObjectInContext;
6+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
7+
8+
public class ContextMapperArchConditions {
9+
10+
public static ModeledAsAggregateInContext beModeledAsAggregatesInCML(BoundedContext cmlContext) {
11+
return ModeledAsAggregateInContext.beModeledAsAggregatesInCML(cmlContext);
12+
}
13+
14+
public static ModeledAsEntityInContext beModeledAsEntityInCML(BoundedContext cmlContext) {
15+
return ModeledAsEntityInContext.beModeledAsEntityInCML(cmlContext);
16+
}
17+
18+
public static ModeledAsValueObjectInContext beModeledAsValueObjectInCML(BoundedContext cmlContext) {
19+
return ModeledAsValueObjectInContext.beModeledAsValueObjectInCML(cmlContext);
20+
}
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.contextmapper.archunit;
2+
3+
import com.tngtech.archunit.lang.ArchRule;
4+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
5+
6+
import static org.contextmapper.archunit.ContextMapperArchConditions.*;
7+
import static org.contextmapper.archunit.conjunctions.JMoleculesClassConjunctions.*;
8+
9+
public class ContextMapperArchRules {
10+
11+
/**
12+
* Ensures that Aggregates in the code (classes annotated with @AggregateRoot) are modeled as Aggregates in CML.
13+
*
14+
* @param boundedContext the Bounded Context within which the Aggregate should be modelled
15+
* @return returns an ArchRule object
16+
*/
17+
public static ArchRule aggregateClassesShouldBeModeledInCml(final BoundedContext boundedContext) {
18+
return aggregateClasses.should(beModeledAsAggregatesInCML(boundedContext));
19+
}
20+
21+
/**
22+
* Ensures that entities in the code (classes annotated with @Entity) are modeled as entities in CML.
23+
*
24+
* @param boundedContext the Bounded Context within which the Aggregate should be modelled
25+
* @return returns an ArchRule object
26+
*/
27+
public static ArchRule entityClassesShouldBeModeledInCml(final BoundedContext boundedContext) {
28+
return entityClasses.should(beModeledAsEntityInCML(boundedContext));
29+
}
30+
31+
/**
32+
* Ensures that value objects in the code (classes annotated with @ValueObject) are modeled as value objects in CML.
33+
*
34+
* @param boundedContext the Bounded Context within which the Aggregate should be modelled
35+
* @return returns an ArchRule object
36+
*/
37+
public static ArchRule valueObjectClassesShouldBeModeledInCml(final BoundedContext boundedContext) {
38+
return valueObjectClasses.should(beModeledAsValueObjectInCML(boundedContext));
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.contextmapper.archunit.cml;
2+
3+
public class BoundedContextDoesNotExistException extends RuntimeException {
4+
5+
public BoundedContextDoesNotExistException(final String name) {
6+
super("A Bounded Context with the name '" + name + "' does not exist in the CML model");
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.contextmapper.archunit.cml;
2+
3+
import org.contextmapper.dsl.cml.CMLResource;
4+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
5+
import org.contextmapper.dsl.standalone.ContextMapperStandaloneSetup;
6+
import org.contextmapper.dsl.standalone.StandaloneContextMapperAPI;
7+
import org.eclipse.xtext.EcoreUtil2;
8+
9+
public class BoundedContextResolver {
10+
11+
public BoundedContext resolveBoundedContextFromModel(final String modelPath, final String boundedContextName) {
12+
StandaloneContextMapperAPI api = ContextMapperStandaloneSetup.getStandaloneAPI();
13+
CMLResource cml = api.loadCML(modelPath);
14+
return EcoreUtil2.eAllOfType(cml.getContextMappingModel(), BoundedContext.class).stream()
15+
.filter(bc -> bc.getName().equals(boundedContextName))
16+
.findAny()
17+
.orElseThrow(() -> new BoundedContextDoesNotExistException(boundedContextName));
18+
}
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2021 The Context Mapper Project Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.contextmapper.archunit.conditions;
17+
18+
import com.tngtech.archunit.core.domain.JavaClass;
19+
import com.tngtech.archunit.lang.ArchCondition;
20+
import com.tngtech.archunit.lang.ConditionEvents;
21+
import com.tngtech.archunit.lang.SimpleConditionEvent;
22+
import org.contextmapper.dsl.contextMappingDSL.Aggregate;
23+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
24+
import org.eclipse.xtext.EcoreUtil2;
25+
26+
import java.util.List;
27+
28+
public class ModeledAsAggregateInContext extends ArchCondition<JavaClass> {
29+
30+
private final BoundedContext cmlContext;
31+
32+
private ModeledAsAggregateInContext(String description, BoundedContext cmlContext) {
33+
super(description, new Object[0]);
34+
this.cmlContext = cmlContext;
35+
}
36+
37+
@Override
38+
public void check(JavaClass javaClass, ConditionEvents events) {
39+
List<Aggregate> aggregates = EcoreUtil2.eAllOfType(cmlContext, Aggregate.class);
40+
events.add(new SimpleConditionEvent(javaClass, aggregates.stream()
41+
.anyMatch(a -> a.getName().equals(javaClass.getSimpleName())),
42+
String.format("The Aggregate '%s' is not modeled in CML.", javaClass.getSimpleName())));
43+
}
44+
45+
public static ModeledAsAggregateInContext beModeledAsAggregatesInCML(BoundedContext cmlContext) {
46+
return new ModeledAsAggregateInContext("be modeled as Aggregate in CML.", cmlContext);
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2021 The Context Mapper Project Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.contextmapper.archunit.conditions;
17+
18+
import com.tngtech.archunit.core.domain.JavaClass;
19+
import com.tngtech.archunit.lang.ArchCondition;
20+
import com.tngtech.archunit.lang.ConditionEvents;
21+
import com.tngtech.archunit.lang.SimpleConditionEvent;
22+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
23+
import org.contextmapper.tactic.dsl.tacticdsl.Entity;
24+
import org.eclipse.xtext.EcoreUtil2;
25+
26+
import java.util.List;
27+
28+
public class ModeledAsEntityInContext extends ArchCondition<JavaClass> {
29+
30+
private final BoundedContext cmlContext;
31+
32+
private ModeledAsEntityInContext(String description, BoundedContext cmlContext) {
33+
super(description, new Object[0]);
34+
this.cmlContext = cmlContext;
35+
}
36+
37+
@Override
38+
public void check(JavaClass javaClass, ConditionEvents events) {
39+
List<Entity> entities = EcoreUtil2.eAllOfType(cmlContext, Entity.class);
40+
events.add(new SimpleConditionEvent(javaClass, entities.stream()
41+
.anyMatch(e -> e.getName().equals(javaClass.getSimpleName())),
42+
String.format("The entity '%s' is not modeled in CML.", javaClass.getSimpleName())));
43+
}
44+
45+
public static ModeledAsEntityInContext beModeledAsEntityInCML(BoundedContext cmlContext) {
46+
return new ModeledAsEntityInContext("be modeled as entity in CML.", cmlContext);
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2021 The Context Mapper Project Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.contextmapper.archunit.conditions;
17+
18+
import com.tngtech.archunit.core.domain.JavaClass;
19+
import com.tngtech.archunit.lang.ArchCondition;
20+
import com.tngtech.archunit.lang.ConditionEvents;
21+
import com.tngtech.archunit.lang.SimpleConditionEvent;
22+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
23+
import org.contextmapper.tactic.dsl.tacticdsl.ValueObject;
24+
import org.eclipse.xtext.EcoreUtil2;
25+
26+
import java.util.List;
27+
28+
public class ModeledAsValueObjectInContext extends ArchCondition<JavaClass> {
29+
30+
private final BoundedContext cmlContext;
31+
32+
private ModeledAsValueObjectInContext(String description, BoundedContext cmlContext) {
33+
super(description, new Object[0]);
34+
this.cmlContext = cmlContext;
35+
}
36+
37+
@Override
38+
public void check(JavaClass javaClass, ConditionEvents events) {
39+
List<ValueObject> valueObjects = EcoreUtil2.eAllOfType(cmlContext, ValueObject.class);
40+
events.add(new SimpleConditionEvent(javaClass, valueObjects.stream()
41+
.anyMatch(vo -> vo.getName().equals(javaClass.getSimpleName())),
42+
String.format("The value object '%s' is not modeled in CML.", javaClass.getSimpleName())));
43+
}
44+
45+
public static ModeledAsValueObjectInContext beModeledAsValueObjectInCML(BoundedContext cmlContext) {
46+
return new ModeledAsValueObjectInContext("be modeled as value object in CML.", cmlContext);
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2021 The Context Mapper Project Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.contextmapper.archunit.conjunctions;
17+
18+
import com.tngtech.archunit.lang.syntax.elements.GivenClassesConjunction;
19+
import org.jmolecules.ddd.annotation.Module;
20+
import org.jmolecules.ddd.annotation.*;
21+
import org.jmolecules.event.annotation.DomainEvent;
22+
23+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
24+
25+
public class JMoleculesClassConjunctions {
26+
27+
public static final GivenClassesConjunction aggregateClasses = classes().that().areAnnotatedWith(AggregateRoot.class);
28+
29+
public static final GivenClassesConjunction moduleClasses = classes().that().areAnnotatedWith(Module.class);
30+
31+
public static final GivenClassesConjunction entityClasses = classes().that().areAnnotatedWith(Entity.class);
32+
33+
public static final GivenClassesConjunction valueObjectClasses = classes().that().areAnnotatedWith(ValueObject.class);
34+
35+
public static final GivenClassesConjunction domainEventClasses = classes().that().areAnnotatedWith(DomainEvent.class);
36+
37+
public static final GivenClassesConjunction serviceClasses = classes().that().areAnnotatedWith(Service.class);
38+
39+
public static final GivenClassesConjunction repositoryClasses = classes().that().areAnnotatedWith(Repository.class);
40+
41+
public static final GivenClassesConjunction factoryClasses = classes().that().areAnnotatedWith(Factory.class);
42+
43+
}

0 commit comments

Comments
 (0)