Skip to content

Commit

Permalink
adds support for rich 'aggregate' modeling
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangsa committed Mar 24, 2024
1 parent 035929f commit d8619a3
Show file tree
Hide file tree
Showing 20 changed files with 570 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

import io.zenwave360.sdk.utils.NamingUtils;
import io.zenwave360.sdk.zdl.ZDLFindUtils;
import io.zenwave360.sdk.zdl.ZDLHttpUtils;
import io.zenwave360.sdk.zdl.ZDLJavaSignatureUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import com.github.jknack.handlebars.Options;
Expand Down Expand Up @@ -42,9 +40,38 @@ public boolean isCrudMethod(String crudMethodPrefix, Options options) {
return isCrudMethod;
}

public List<Map> serviceAggregates(Map service, Options options) {
var zdl = options.get("zdl");
var aggregateNames = JSONPath.get(service, "aggregates", Collections.emptyList());
return aggregateNames.stream()
.map(aggregateName -> JSONPath.get(zdl, "$.allEntitiesAndEnums." + aggregateName, Map.of()))
.filter(aggregate -> "aggregates".equals(aggregate.get("type")))
.collect(Collectors.toList());
}

public boolean includeDomainEvents(Map service, Options options) {
var zdl = options.get("zdl");
return !JSONPath.get(zdl, "$.aggregates[*].commands[*].withEvents", List.of()).isEmpty();
}

public List<Map> aggregateEvents(Map<String, Object> aggregate, Options options) {
var zdl = options.get("zdl");
return ZDLFindUtils.aggregateEvents(aggregate).stream().map(event -> (Map) JSONPath.get(zdl, "$.events." + event)).toList();
}

public Collection<String> findAggregateInputs(Map aggregate, Options options) {
return new HashSet<>(JSONPath.get(aggregate, "$.commands[*].parameter", List.of()));
}

public String findEntityAggregate(String entityName, Options options) {
var zdl = options.get("zdl");
var aggregateName = (String) aggregate.get("name");
var aggregateNames = JSONPath.get(zdl, "$.aggregates[*][?(@.aggregateRoot == '" + entityName + "')].name", List.of());
return aggregateNames.isEmpty()? null : (String) aggregateNames.get(0);
}

public Collection<String> findServiceInputs(Map entity, Options options) {
var zdl = options.get("zdl");
var aggregateName = (String) entity.get("name");
var inputDTOSuffix = (String) options.get("inputDTOSuffix");
Set<String> inputs = new HashSet<String>();
inputs.addAll(JSONPath.get(zdl, "$.services[*][?('" + aggregateName + "' in @.aggregates)].methods[*].parameter"));
Expand Down Expand Up @@ -305,14 +332,14 @@ public String validationPatternJava(String pattern, Options options) {
return pattern.replace("\\", "").replace("\\", "\\\\");
}

public Object skipEntityRepository(Object context, Options options) {
Map entity = (Map) context;
return generator.skipEntityRepository.apply(Map.of("entity", entity));
public Object skipEntityRepository(Map entity, Options options) {
var zdl = options.get("zdl");
return generator.skipEntityRepository.apply(Map.of("zdl", zdl, "entity", entity));
};

public Object skipEntityId(Object context, Options options) {
Map entity = (Map) context;
return generator.skipEntityId.apply(Map.of("entity", entity));
public Object skipEntityId(Map entity, Options options) {
var zdl = options.get("zdl");
return generator.skipEntityId.apply(Map.of("zdl", zdl, "entity", entity));
};

public Object addExtends(Object entity, Options options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.zenwave360.sdk.options.PersistenceType;
import io.zenwave360.sdk.options.ProgrammingStyle;
import io.zenwave360.sdk.utils.JSONPath;
import io.zenwave360.sdk.zdl.ZDLFindUtils;

/**
* Generates a backend application with the following structure:
Expand Down Expand Up @@ -48,6 +49,7 @@ public class BackendDefaultApplicationGenerator extends AbstractZDLProjectGenera

public String configPackage = "{{basePackage}}.config";
public String entitiesPackage = "{{basePackage}}.core.domain";
public String domainEventsPackage = "{{basePackage}}.core.domain.events";
public String inboundPackage = "{{basePackage}}.core.inbound";
public String inboundDtosPackage = "{{basePackage}}.core.inbound.dtos";
public String outboundPackage = "{{basePackage}}.core.outbound";
Expand Down Expand Up @@ -91,7 +93,7 @@ protected boolean is(Map<String, Object> model, String... annotations) {
return !(JSONPath.get(model, "$.entity.options[?(" + annotationsFilter + ")]", List.of())).isEmpty();
}

protected Function<Map<String, Object>, Boolean> skipEntityRepository = (model) -> !is(model, "aggregate");
protected Function<Map<String, Object>, Boolean> skipEntityRepository = (model) -> !(is(model, "aggregate") || ZDLFindUtils.isAggregateRoot(JSONPath.get(model, "zdl"), JSONPath.get(model, "$.entity.name")));
protected Function<Map<String, Object>, Boolean> skipEntityId = (model) -> is(model, "embedded", "vo", "input", "abstract");
protected Function<Map<String, Object>, Boolean> skipEntity = (model) -> is(model, "vo", "input");
protected Function<Map<String, Object>, Boolean> skipEntityInput = (model) -> inputDTOSuffix == null || inputDTOSuffix.isEmpty();
Expand All @@ -102,6 +104,11 @@ protected boolean is(Map<String, Object> model, String... annotations) {
protected ZDLProjectTemplates configureProjectTemplates() {
var ts = new ZDLProjectTemplates("io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator");

ts.addTemplate(ts.aggregateTemplates, "src/main/java","core/domain/common/Aggregate.java",
"{{asPackageFolder entitiesPackage}}/{{aggregate.name}}.java", JAVA, null, true);
ts.addTemplate(ts.domainEventsTemplates, "src/main/java","core/domain/common/DomainEvent.java",
"{{asPackageFolder domainEventsPackage}}/{{event.name}}.java", JAVA, null, true);

ts.addTemplate(ts.entityTemplates, "src/main/java","core/domain/{{persistence}}/Entity.java",
"{{asPackageFolder entitiesPackage}}/{{entity.name}}.java", JAVA, skipEntity, false);
ts.addTemplate(ts.entityTemplates, "src/main/java","core/outbound/{{persistence}}/{{style}}/EntityRepository.java",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public class BackendMultiModuleApplicationGenerator extends BackendDefaultApplic
protected ZDLProjectTemplates configureProjectTemplates() {
var ts = new ZDLProjectTemplates("io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator");

ts.addTemplate(ts.aggregateTemplates, "src/main/java","core/domain/common/Aggregate.java","{{mavenModulesPrefix}}-domain",
"{{asPackageFolder entitiesPackage}}/{{aggregate.name}}.java", JAVA, null, true);
ts.addTemplate(ts.domainEventsTemplates, "src/main/java","core/domain/common/DomainEvent.java","{{mavenModulesPrefix}}-domain",
"{{asPackageFolder domainEventsPackage}}/{{event.name}}.java", JAVA, null, true);

ts.addTemplate(ts.entityTemplates, "src/main/java","core/domain/{{persistence}}/Entity.java", "{{mavenModulesPrefix}}-domain",
"{{asPackageFolder entitiesPackage}}/{{entity.name}}.java", JAVA, skipEntity, false);
ts.addTemplate(ts.entityTemplates, "src/main/java","core/outbound/{{persistence}}/{{style}}/EntityRepository.java", "{{mavenModulesPrefix}}-domain",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package {{entitiesPackage}};

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import {{domainEventsPackage}}.*;
import {{inboundDtosPackage}}.*;

import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;


public class {{aggregate.name}} {
private static final Mapper mapper = Mappers.getMapper(Mapper.class);

private final {{aggregate.aggregateRoot}} rootEntity;

private final List<Object> events = new ArrayList<>();

public {{aggregate.name}}() {
this(new {{aggregate.aggregateRoot}}());
}
public {{aggregate.name}}({{aggregate.aggregateRoot}} rootEntity) {
this.rootEntity = rootEntity;
}

public String getId() {
return rootEntity.getId();
}

public {{aggregate.aggregateRoot}} getRootEntity() {
return rootEntity;
}

public List<?> getEvents() {
return Collections.unmodifiableList(events);
}

{{#each aggregate.commands as |method|}}
{{~> (partial '../../implementation/partials/serviceMethodJavadoc')}}
public void {{method.name}}({{method.parameter}} input) {
// TODO: implement this command
mapper.update(rootEntity, input);
{{~#each (methodEvents method) as |event|}}
events.add(mapper.as{{event.name}}(rootEntity));
{{~/each}}
}
{{/each}}

@org.mapstruct.Mapper
interface Mapper {
{{~#each (findAggregateInputs aggregate) as |input|}}
{{aggregate.aggregateRoot}} update(@MappingTarget {{aggregate.aggregateRoot}} entity, {{input}} input);
{{~/each}}

{{~#each (aggregateEvents aggregate) as |event|}}
{{event.className}} as{{event.name}}({{aggregate.aggregateRoot}} entity);
{{~/each}}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package {{domainEventsPackage}};

import java.io.Serializable;
import java.math.*;
import java.time.*;
import java.util.*;
import jakarta.validation.constraints.*;

import {{entitiesPackage}}.*;

/**
* {{event.comment}}
*/
{{~#if useLombok}}
@lombok.Getter @lombok.Setter
{{~/if}}
public {{abstractClass event}} class {{event.className}} {{addExtends event}} implements Serializable {

@java.io.Serial
private static final long serialVersionUID = 1L;

{{#each event.fields as |field|}}
{{#each field.validations as |validation|~}}
// @{{validation.name}}("{{validation.value}}")
{{~/each}}
private {{{fieldType field}}} {{field.name}};
{{/each}}


{{#each event.fields as |field|}}
{{~#if field.isArray}}
public {{event.className}} add{{capitalize field.name}}({{javaType field}} {{field.name}}) {
this.{{field.name}}.add({{field.name}});
return this;
}
{{~/if}}
{{/each}}

{{~#unless useLombok}}
{{#each event.fields as |field|}}
public {{{fieldType field}}} get{{capitalize field.name}}() {
return {{field.name}};
}

public {{event.className}} set{{capitalize field.name}}({{{fieldType field}}} {{field.name}}) {
this.{{field.name}} = {{field.name}};
return this;
}
{{/each}}
{{~/unless}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {{outboundPackage}}.jpa.*;
{{#if includeEmitEventsImplementation}}
import {{outboundEventsPackage}}.*;
{{/if}}

{{#if (includeDomainEvents service)}}
import {{domainEventsPackage}}.*;
{{/if}}

import java.math.*;
import java.time.*;
Expand All @@ -22,6 +24,9 @@ import org.springframework.data.domain.Pageable;
import java.util.concurrent.CompletableFuture;
import org.springframework.scheduling.annotation.Async;
{{~/if}}
{{~#if (includeEmitEventsImplementation service)}}
import org.springframework.context.ApplicationEventPublisher;
{{~/if}}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -48,19 +53,22 @@ public class {{service.name}}Impl implements {{service.name}} {
{{#if (includeEmitEventsImplementation service)}}
private final EventsMapper eventsMapper = EventsMapper.INSTANCE;
private final {{eventsProducerInterface service.name}} eventsProducer;
private final ApplicationEventPublisher applicationEventPublisher;
{{/if}}

{{#unless useLombok~}}
/**
* Constructor.
*/
public {{service.name}}Impl({{#joinWithTemplate service.entities delimiter=", "}}{{#unless (skipEntityRepository this)}}{{className}}Repository {{instanceName}}Repository{{/unless}}{{/joinWithTemplate}}
{{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer{{/if}}) {
{{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer, ApplicationEventPublisher applicationEventPublisher{{/if}}
) {
{{~#joinWithTemplate service.entities ~}}
{{#unless (skipEntityRepository this)}}this.{{instanceName}}Repository = {{instanceName}}Repository;{{/unless}}
{{~/joinWithTemplate~}}
{{~#if (includeEmitEventsImplementation service)}}
this.eventsProducer = eventsProducer;
this.applicationEventPublisher = applicationEventPublisher;
{{~/if}}
}
{{/unless~}}
Expand All @@ -72,4 +80,26 @@ public class {{service.name}}Impl implements {{service.name}} {
}
{{/each}}

{{#each (serviceAggregates service) as |aggregate|}}
private {{aggregate.name}} persistAndEmitEvents({{aggregate.name}} {{asInstanceName aggregate.name}}) {
var {{asInstanceName aggregate.aggregateRoot}} = {{asInstanceName aggregate.aggregateRoot}}Repository.save({{asInstanceName aggregate.name}}.getRootEntity());
{{asInstanceName aggregate.name}}.getEvents().forEach(event -> {
{{#each (aggregateEvents aggregate) as |event|}}
if (event instanceof {{event.className}}) {
{{~#if event.options.asyncapi }}
{{~#if includeEmitEventsImplementation }}
eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}});
{{~else}}
// TODO: set 'includeEmitEventsImplementation' to generate this
// eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}});
{{~/if}}
{{~else}}
applicationEventPublisher.publishEvent(event);
{{~/if}}
}
{{/each}}
});
return {{asInstanceName aggregate.name}};
}
{{/each}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import org.springframework.data.domain.Page;
public interface {{service.name}}Mapper {

{{service.name}}Mapper INSTANCE = Mappers.getMapper({{service.name}}Mapper.class);
{{~#each service.aggregates as |entityName|}}
{{~#each service.entityNames as |entityName|}}
{{~assign "entity" (findEntity entityName)}}

{{~#each (findAggregateInputs entity) as |input|}}
{{~#each (findServiceInputs entity) as |input|}}
{{~#if (not (eq entity.className input))}}
// {{entity.className}} as{{entity.className}}({{mapperInputSignature input}});
{{~/if}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {{outboundPackage}}.mongodb.*;
{{#if includeEmitEventsImplementation}}
import {{outboundEventsPackage}}.*;
{{/if}}
{{#if (includeDomainEvents service)}}
import {{domainEventsPackage}}.*;
{{/if}}

import java.math.*;
import java.time.*;
Expand All @@ -21,6 +24,9 @@ import org.springframework.data.domain.Pageable;
import java.util.concurrent.CompletableFuture;
import org.springframework.scheduling.annotation.Async;
{{~/if}}
{{~#if (includeEmitEventsImplementation service)}}
import org.springframework.context.ApplicationEventPublisher;
{{~/if}}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -47,19 +53,22 @@ public class {{service.name}}Impl implements {{service.name}} {
{{#if (includeEmitEventsImplementation service)}}
private final EventsMapper eventsMapper = EventsMapper.INSTANCE;
private final {{eventsProducerInterface service.name}} eventsProducer;
private final ApplicationEventPublisher applicationEventPublisher;
{{/if}}

{{#unless useLombok~}}
/**
* Constructor.
*/
public {{service.name}}Impl({{#joinWithTemplate service.entities delimiter=", "}}{{#unless (skipEntityRepository this)}}{{className}}Repository {{instanceName}}Repository{{/unless}}{{/joinWithTemplate}}
{{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer{{/if}}) {
{{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer, ApplicationEventPublisher applicationEventPublisher{{/if}}
) {
{{~#joinWithTemplate service.entities ~}}
{{#unless (skipEntityRepository this)}}this.{{instanceName}}Repository = {{instanceName}}Repository;{{/unless}}
{{~/joinWithTemplate~}}
{{~#if (includeEmitEventsImplementation service)}}
this.eventsProducer = eventsProducer;
this.applicationEventPublisher = applicationEventPublisher;
{{~/if}}
}
{{/unless~}}
Expand All @@ -70,4 +79,27 @@ public class {{service.name}}Impl implements {{service.name}} {
{{~> (partial '../../partials/mongodbMethodBody')~}}
}
{{/each}}

{{#each (serviceAggregates service) as |aggregate|}}
private {{aggregate.name}} persistAndEmitEvents({{aggregate.name}} {{asInstanceName aggregate.name}}) {
var {{asInstanceName aggregate.aggregateRoot}} = {{asInstanceName aggregate.aggregateRoot}}Repository.save({{asInstanceName aggregate.name}}.getRootEntity());
{{asInstanceName aggregate.name}}.getEvents().forEach(event -> {
{{#each (aggregateEvents aggregate) as |event|}}
if (event instanceof {{event.className}}) {
{{~#if event.options.asyncapi }}
{{~#if includeEmitEventsImplementation }}
eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}});
{{~else}}
// TODO: set 'includeEmitEventsImplementation' to generate this
// eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}});
{{~/if}}
{{~else}}
applicationEventPublisher.publishEvent(event);
{{~/if}}
}
{{/each}}
});
return {{asInstanceName aggregate.name}};
}
{{/each}}
}
Loading

0 comments on commit d8619a3

Please sign in to comment.