Skip to content

Commit ea4257a

Browse files
authored
Aggregate Structure Check (#2)
1 parent a1ebb08 commit ea4257a

22 files changed

+448
-22
lines changed

README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,24 +128,23 @@ public class ExampleArchitectureTest {
128128
```
129129

130130
## Available Rules (just a PoC for now ;)
131-
| Rule | Description |
132-
|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
133-
| `aggregatesShouldBeModeledInCML` | Aggregates that are implemented in the code (for example annotated with @AggregateRoot jMolecules annotation) shall exist in the CML Bounded Context as well. |
134-
| `modulesShouldBeModeledInCML` | Modules that are implemented in the code (for example annotated with @Module jMolecules annotation) shall exist in the CML Bounded Context as well. |
135-
| `entitiesShouldBeModeledInCML` | Entities that are implemented in the code (for example annotated with @Entity jMolecules annotation) shall exist in the CML Bounded Context as well. |
136-
| `valueObjectsShouldBeModeledInCML` | Value Objects that are implemented in the code (for example annotated with @ValueObject jMolecules annotation) shall exist in the CML Bounded Context as well. |
137-
| `domainEventsShouldBeModeledInCML` | Domain events that are implemented in the code (for example annotated with @DomainEvent jMolecules annotation) shall exist int the CML Bounded Context as well. |
138-
| `servicesShouldBeModeledInCML` | Services that are implemented in the code (for example annotated with @Service jMolecules annotation) shall exist in the CML Bounded Context as well. |
139-
| `repositoriesShouldBeModeledInCML` | Repositories that are implemented in the code (for example annotated with @Repository annotation) shall exist in the CML Bounded Context as well. |
140-
| | |
141-
| | |
131+
| | Description |
132+
|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
133+
| `aggregatesShouldBeModeledInCML` | Aggregates that are implemented in the code (for example annotated with @AggregateRoot jMolecules annotation) shall exist in the CML Bounded Context as well. |
134+
| `modulesShouldBeModeledInCML` | Modules that are implemented in the code (for example annotated with @Module jMolecules annotation) shall exist in the CML Bounded Context as well. |
135+
| `entitiesShouldBeModeledInCML` | Entities that are implemented in the code (for example annotated with @Entity jMolecules annotation) shall exist in the CML Bounded Context as well. |
136+
| `valueObjectsShouldBeModeledInCML` | Value Objects that are implemented in the code (for example annotated with @ValueObject jMolecules annotation) shall exist in the CML Bounded Context as well. |
137+
| `domainEventsShouldBeModeledInCML` | Domain events that are implemented in the code (for example annotated with @DomainEvent jMolecules annotation) shall exist int the CML Bounded Context as well. |
138+
| `servicesShouldBeModeledInCML` | Services that are implemented in the code (for example annotated with @Service jMolecules annotation) shall exist in the CML Bounded Context as well. |
139+
| `repositoriesShouldBeModeledInCML` | Repositories that are implemented in the code (for example annotated with @Repository annotation) shall exist in the CML Bounded Context as well. |
140+
| `aggregatesShouldAdhereToCmlAggregateStructure` | This rule ensures that an Aggregate in the code (basically a Java package with a marker on the aggregate root entity; for example annotated with @AggregateRoot jMolecules annotation) consists of the same entities, value objects, and domain events as it is modeled in CML. |
141+
| | |
142142

143143
## Ideas for rules to implement
144144
The following list states some rules that could be implemented. We are still working on this list and input it very welcome!
145145

146146
| Rule | Description |
147147
|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------|
148-
| `aggregatesShouldAdhereToCmlModel` | Structural check that ensures that Aggregates consists of same objects (Entities, ValueObjects, etc.); code vs. CML |
149148
| `entityShouldAdhereToCmlStructure` | Structural check of entity: same attributes, same methods, same references |
150149
| `valueObjectShouldAdhereToCmlStructure` | Structure check of value object: same attributes, same methods, same references |
151150
| `{}ShouldAdhereToCmlStructure` | dito. for other tactic DDD objects (domain events, service, etc.) |

src/main/java/org/contextmapper/archunit/AbstractTacticArchUnitTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import org.junit.jupiter.api.BeforeEach;
2424
import org.junit.jupiter.api.Test;
2525

26-
import static org.contextmapper.archunit.ContextMapperArchRules.*;
26+
import static org.contextmapper.archunit.ContextMapperJMoleculesArchRules.*;
2727

2828
/**
2929
* An abstract test class that can be extended in order to execute all our ArchRules for a single Bounded Context.
30+
* This test is based on the jMolecules annotations. In case you work with other annotations, you have to implement
31+
* your test manually.
3032
*/
3133
public abstract class AbstractTacticArchUnitTest {
3234

@@ -69,6 +71,16 @@ void aggregatesShouldBeModeledInCML() {
6971
aggregateClassesShouldBeModeledInCml(context).check(classes);
7072
}
7173

74+
@Test
75+
void aggregatesShouldAdhereToCmlAggregateStructure() {
76+
aggregatesShouldAdhereToCmlStructure(context).check(classes);
77+
}
78+
79+
@Test
80+
void modulesShouldBeModeledInCML() {
81+
modulePackagesShouldBeModeledInCml(context).check(classes);
82+
}
83+
7284
@Test
7385
void entitiesShouldBeModeledInCML() {
7486
entityClassesShouldBeModeledInCml(context).check(classes);
@@ -78,4 +90,19 @@ void entitiesShouldBeModeledInCML() {
7890
void valueObjectsShouldBeModeledInCML() {
7991
valueObjectClassesShouldBeModeledInCml(context).check(classes);
8092
}
93+
94+
@Test
95+
void domainEventsShouldBeModeledInCML() {
96+
domainEventClassesShouldBeModeledInCml(context).check(classes);
97+
}
98+
99+
@Test
100+
void servicesShouldBeModeledInCML() {
101+
serviceClassesShouldBeModeledInCml(context).check(classes);
102+
}
103+
104+
@Test
105+
void repositoriesShouldBeModeledInCML() {
106+
repositoryClassesShouldBeModeledInCml(context).check(classes);
107+
}
81108
}

src/main/java/org/contextmapper/archunit/ContextMapperArchConditions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.contextmapper.archunit;
1717

18+
import org.contextmapper.archunit.annotations.TacticDDDAnnotationSet;
1819
import org.contextmapper.archunit.conditions.*;
1920
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
2021

@@ -48,4 +49,9 @@ public static ModeledAsRepositoryInContext beModeledAsRepositoryInCML(BoundedCon
4849
return ModeledAsRepositoryInContext.beModeledAsRepositoryInCML(boundedContext);
4950
}
5051

52+
public static AdhereToCmlAggregateStructure adhereToCmlAggregateStructure(BoundedContext boundedContext,
53+
TacticDDDAnnotationSet tacticDDDAnnotationSet) {
54+
return AdhereToCmlAggregateStructure.adhereToCmlAggregateStructure(boundedContext, tacticDDDAnnotationSet);
55+
}
56+
5157
}

src/main/java/org/contextmapper/archunit/ContextMapperArchRules.java renamed to src/main/java/org/contextmapper/archunit/ContextMapperJMoleculesArchRules.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
package org.contextmapper.archunit;
1717

1818
import com.tngtech.archunit.lang.ArchRule;
19+
import org.contextmapper.archunit.annotations.JMoleculesTacticAnnotationSet;
1920
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
2021

22+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all;
2123
import static org.contextmapper.archunit.ContextMapperArchConditions.*;
2224
import static org.contextmapper.archunit.conjunctions.JMoleculesClassConjunctions.*;
25+
import static org.contextmapper.archunit.transformers.JMoleculesAggregatePackageTransformer.aggregatePackages;
2326

24-
public class ContextMapperArchRules {
27+
public class ContextMapperJMoleculesArchRules {
2528

2629
/**
2730
* Ensures that Aggregates in the code (classes annotated with @AggregateRoot) are modeled as Aggregates in CML.
@@ -74,7 +77,7 @@ public static ArchRule domainEventClassesShouldBeModeledInCml(final BoundedConte
7477
}
7578

7679
/**
77-
* Ensures that services in the code (classes annotated with @Service) are modeled as service in CML.
80+
* Ensures that services in the code (classes annotated with @Service) are modeled as services in CML.
7881
*
7982
* @param boundedContext the Bounded Context within which the Aggregate should be modelled
8083
* @return returns an ArchRule object
@@ -93,4 +96,15 @@ public static ArchRule repositoryClassesShouldBeModeledInCml(final BoundedContex
9396
return repositoryClasses.should(beModeledAsRepositoryInCML(boundedContext));
9497
}
9598

99+
/**
100+
* Ensures that the classes (entities, value objects, and domain events) inside an Aggregates package are also
101+
* contained in the corresponding Aggregate of the CML model.
102+
*
103+
* @param boundedContext the Bounded Context within which the Aggregate should be modelled
104+
* @return returns an ArchRule object
105+
*/
106+
public static ArchRule aggregatesShouldAdhereToCmlStructure(final BoundedContext boundedContext) {
107+
return all(aggregatePackages).should(adhereToCmlAggregateStructure(boundedContext, JMoleculesTacticAnnotationSet.instance()));
108+
}
109+
96110
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.contextmapper.archunit.annotations;
2+
3+
import org.jmolecules.ddd.annotation.Module;
4+
import org.jmolecules.ddd.annotation.*;
5+
import org.jmolecules.event.annotation.DomainEvent;
6+
7+
import java.lang.annotation.Annotation;
8+
9+
public class JMoleculesTacticAnnotationSet implements TacticDDDAnnotationSet {
10+
11+
@Override
12+
public Class<? extends Annotation> aggregateRootAnnotation() {
13+
return AggregateRoot.class;
14+
}
15+
16+
@Override
17+
public Class<? extends Annotation> entityAnnotation() {
18+
return Entity.class;
19+
}
20+
21+
@Override
22+
public Class<? extends Annotation> valueObjectAnnotation() {
23+
return ValueObject.class;
24+
}
25+
26+
@Override
27+
public Class<? extends Annotation> domainEventAnnotation() {
28+
return DomainEvent.class;
29+
}
30+
31+
@Override
32+
public Class<? extends Annotation> serviceAnnotation() {
33+
return Service.class;
34+
}
35+
36+
@Override
37+
public Class<? extends Annotation> moduleAnnotation() {
38+
return Module.class;
39+
}
40+
41+
@Override
42+
public Class<? extends Annotation> repositoryAnnotation() {
43+
return Repository.class;
44+
}
45+
46+
public static TacticDDDAnnotationSet instance() {
47+
return new JMoleculesTacticAnnotationSet();
48+
}
49+
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.contextmapper.archunit.annotations;
2+
3+
import java.lang.annotation.Annotation;
4+
5+
public interface TacticDDDAnnotationSet {
6+
7+
Class<? extends Annotation> aggregateRootAnnotation();
8+
9+
Class<? extends Annotation> entityAnnotation();
10+
11+
Class<? extends Annotation> valueObjectAnnotation();
12+
13+
Class<? extends Annotation> domainEventAnnotation();
14+
15+
Class<? extends Annotation> serviceAnnotation();
16+
17+
Class<? extends Annotation> moduleAnnotation();
18+
19+
Class<? extends Annotation> repositoryAnnotation();
20+
21+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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.core.domain.JavaPackage;
20+
import com.tngtech.archunit.lang.ArchCondition;
21+
import com.tngtech.archunit.lang.ConditionEvents;
22+
import com.tngtech.archunit.lang.SimpleConditionEvent;
23+
import org.contextmapper.archunit.annotations.TacticDDDAnnotationSet;
24+
import org.contextmapper.dsl.contextMappingDSL.Aggregate;
25+
import org.contextmapper.dsl.contextMappingDSL.BoundedContext;
26+
import org.contextmapper.tactic.dsl.tacticdsl.DomainEvent;
27+
import org.contextmapper.tactic.dsl.tacticdsl.DomainObject;
28+
import org.contextmapper.tactic.dsl.tacticdsl.Entity;
29+
import org.contextmapper.tactic.dsl.tacticdsl.ValueObject;
30+
import org.eclipse.xtext.EcoreUtil2;
31+
32+
import java.lang.annotation.Annotation;
33+
import java.util.Optional;
34+
import java.util.stream.Collectors;
35+
36+
public class AdhereToCmlAggregateStructure extends ArchCondition<JavaPackage> {
37+
38+
private final BoundedContext cmlContext;
39+
private final TacticDDDAnnotationSet tacticDDDAnnotationTypes;
40+
41+
private AdhereToCmlAggregateStructure(String description, BoundedContext cmlContext,
42+
TacticDDDAnnotationSet tacticDDDAnnotationTypes) {
43+
super(description, new Object[0]);
44+
this.cmlContext = cmlContext;
45+
this.tacticDDDAnnotationTypes = tacticDDDAnnotationTypes;
46+
}
47+
48+
@Override
49+
public void check(JavaPackage javaPackage, ConditionEvents events) {
50+
Optional<JavaClass> aggregateRootClass = findAggregateRootClass(javaPackage, events);
51+
if (!aggregateRootClass.isPresent())
52+
return;
53+
54+
Optional<Aggregate> cmlAggregate = findCmlAggregate(javaPackage, events, aggregateRootClass);
55+
if (!cmlAggregate.isPresent())
56+
return;
57+
58+
checkEntities(cmlAggregate.get(), javaPackage, events);
59+
checkValueObjects(cmlAggregate.get(), javaPackage, events);
60+
checkDomainEvents(cmlAggregate.get(), javaPackage, events);
61+
}
62+
63+
private void checkEntities(Aggregate cmlAggregate, JavaPackage javaPackage, ConditionEvents events) {
64+
checkDomainObject(cmlAggregate, javaPackage, events, this.tacticDDDAnnotationTypes.entityAnnotation(), Entity.class);
65+
}
66+
67+
private void checkValueObjects(Aggregate cmlAggregate, JavaPackage javaPackage, ConditionEvents events) {
68+
checkDomainObject(cmlAggregate, javaPackage, events, this.tacticDDDAnnotationTypes.valueObjectAnnotation(), ValueObject.class);
69+
}
70+
71+
private void checkDomainEvents(Aggregate cmlAggregate, JavaPackage javaPackage, ConditionEvents events) {
72+
checkDomainObject(cmlAggregate, javaPackage, events, this.tacticDDDAnnotationTypes.domainEventAnnotation(), DomainEvent.class);
73+
}
74+
75+
private void checkDomainObject(Aggregate cmlAggregate, JavaPackage javaPackage, ConditionEvents events,
76+
Class<? extends Annotation> annotation, Class<? extends DomainObject> cmlType) {
77+
for (JavaClass domainObjectClass : javaPackage.getClasses().stream()
78+
.filter(c -> c.isAnnotatedWith(annotation))
79+
.collect(Collectors.toSet())) {
80+
Optional<? extends DomainObject> cmlObject = EcoreUtil2.eAllOfType(cmlAggregate, cmlType).stream()
81+
.filter(domainObject -> domainObject.getName().equals(domainObjectClass.getSimpleName()))
82+
.findAny();
83+
events.add(new SimpleConditionEvent(domainObjectClass, cmlObject.isPresent(),
84+
String.format("The Aggregate '%s' does not contain a " + cmlType.getSimpleName() + " '%s' in CML.", cmlAggregate.getName(),
85+
domainObjectClass.getSimpleName())));
86+
}
87+
}
88+
89+
private Optional<Aggregate> findCmlAggregate(JavaPackage javaPackage, ConditionEvents events, Optional<JavaClass> aggregateRootClass) {
90+
Optional<Aggregate> cmlAggregate = EcoreUtil2.eAllOfType(cmlContext, Aggregate.class).stream()
91+
.filter(a -> a.getName().equals(aggregateRootClass.get().getSimpleName()))
92+
.findAny();
93+
events.add(new SimpleConditionEvent(javaPackage, cmlAggregate.isPresent(),
94+
String.format("The aggregate '%s' is not modeled in CML.", aggregateRootClass.get().getSimpleName())));
95+
return cmlAggregate;
96+
}
97+
98+
private Optional<JavaClass> findAggregateRootClass(JavaPackage javaPackage, ConditionEvents events) {
99+
Optional<JavaClass> aggregateRootClass = javaPackage.getClasses().stream()
100+
.filter(c -> c.isAnnotatedWith(tacticDDDAnnotationTypes.aggregateRootAnnotation()))
101+
.findAny();
102+
events.add(new SimpleConditionEvent(javaPackage, aggregateRootClass.isPresent(),
103+
String.format("The aggregate root class for package '%s' cannot be found.", javaPackage.getName())));
104+
return aggregateRootClass;
105+
}
106+
107+
public static AdhereToCmlAggregateStructure adhereToCmlAggregateStructure(BoundedContext cmlContext,
108+
TacticDDDAnnotationSet tacticDDDAnnotationTypes) {
109+
return new AdhereToCmlAggregateStructure("adhere to CML Aggregate structure.", cmlContext, tacticDDDAnnotationTypes);
110+
}
111+
112+
}

0 commit comments

Comments
 (0)