@fornari-lanza/domain-design-language-language 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/out/domain-design-language-formatter.d.ts +5 -0
- package/out/domain-design-language-formatter.js +197 -0
- package/out/domain-design-language-formatter.js.map +1 -0
- package/out/domain-design-language-module.d.ts +41 -0
- package/out/domain-design-language-module.js +50 -0
- package/out/domain-design-language-module.js.map +1 -0
- package/out/domain-design-language-scope.d.ts +8 -0
- package/out/domain-design-language-scope.js +79 -0
- package/out/domain-design-language-scope.js.map +1 -0
- package/out/domain-design-language-validator.d.ts +39 -0
- package/out/domain-design-language-validator.js +468 -0
- package/out/domain-design-language-validator.js.map +1 -0
- package/out/generated/ast.d.ts +1393 -0
- package/out/generated/ast.js +1147 -0
- package/out/generated/ast.js.map +1 -0
- package/out/generated/grammar.d.ts +6 -0
- package/out/generated/grammar.js +3430 -0
- package/out/generated/grammar.js.map +1 -0
- package/out/generated/module.d.ts +13 -0
- package/out/generated/module.js +21 -0
- package/out/generated/module.js.map +1 -0
- package/out/index.d.ts +5 -0
- package/out/index.js +6 -0
- package/out/index.js.map +1 -0
- package/package.json +49 -0
- package/src/domain-design-language-formatter.ts +186 -0
- package/src/domain-design-language-module.ts +96 -0
- package/src/domain-design-language-scope.ts +132 -0
- package/src/domain-design-language-validator.ts +699 -0
- package/src/domain-design-language.langium +299 -0
- package/src/generated/ast.ts +1737 -0
- package/src/generated/grammar.ts +3432 -0
- package/src/generated/module.ts +25 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import type { AstNode, ValidationAcceptor, ValidationChecks } from 'langium';
|
|
2
|
+
import {
|
|
3
|
+
AggregateRoot,
|
|
4
|
+
Command,
|
|
5
|
+
AsymmetricRelationship,
|
|
6
|
+
BoundedContext,
|
|
7
|
+
DomainEvent,
|
|
8
|
+
DomainEventsDeclaration,
|
|
9
|
+
DomainModel,
|
|
10
|
+
DomainService,
|
|
11
|
+
Entity,
|
|
12
|
+
Enum,
|
|
13
|
+
EventStormingDeclaration,
|
|
14
|
+
isActor,
|
|
15
|
+
isAggregateRoot,
|
|
16
|
+
isAsymmetricRelationship,
|
|
17
|
+
isExternalSystem,
|
|
18
|
+
isPolicyTrigger,
|
|
19
|
+
//isPortReferenceDependency,
|
|
20
|
+
isProjection,
|
|
21
|
+
isReadModel,
|
|
22
|
+
isSymmetricRelationship,
|
|
23
|
+
isValueObject,
|
|
24
|
+
Payload,
|
|
25
|
+
PolicyTrigger,
|
|
26
|
+
ProcessFlow,
|
|
27
|
+
ReactionStep,
|
|
28
|
+
Relationship,
|
|
29
|
+
UbiquitousLanguageDeclaration,
|
|
30
|
+
ValueObject,
|
|
31
|
+
type ContextMap,
|
|
32
|
+
type DomainDesignLanguageAstType,
|
|
33
|
+
MethodsDeclaration,
|
|
34
|
+
Method,
|
|
35
|
+
isMethod,
|
|
36
|
+
ExceptionsDeclaration,
|
|
37
|
+
} from './generated/ast.js';
|
|
38
|
+
import type { DomainDesignLanguageServices } from './domain-design-language-module.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register custom validation checks.
|
|
42
|
+
*/
|
|
43
|
+
export function registerValidationChecks(
|
|
44
|
+
services: DomainDesignLanguageServices,
|
|
45
|
+
) {
|
|
46
|
+
const registry = services.validation.ValidationRegistry;
|
|
47
|
+
const validator = services.validation.DomainDesignLanguageValidator;
|
|
48
|
+
const checks: ValidationChecks<DomainDesignLanguageAstType> = {
|
|
49
|
+
DomainModel: validator.checkUniquePurpose,
|
|
50
|
+
BoundedContext: [
|
|
51
|
+
validator.checkUniqueAggregates,
|
|
52
|
+
validator.checkUniqueDomainServices,
|
|
53
|
+
validator.checkUniquePorts,
|
|
54
|
+
validator.checkUniqueCommands,
|
|
55
|
+
validator.checkNameStartsWithCapital,
|
|
56
|
+
],
|
|
57
|
+
UbiquitousLanguageDeclaration: validator.checkUniqueTerms,
|
|
58
|
+
Term: validator.checkNameStartsWithCapital,
|
|
59
|
+
ContextMap: validator.checkUniqueRelationships,
|
|
60
|
+
Relationship: validator.checkSelfRelationships,
|
|
61
|
+
AsymmetricRelationship: validator.checkRelationshipRoles,
|
|
62
|
+
AggregateRoot: [
|
|
63
|
+
validator.checkNameStartsWithCapital,
|
|
64
|
+
validator.checkUniqueAttributes,
|
|
65
|
+
],
|
|
66
|
+
ValueObject: [
|
|
67
|
+
validator.checkNameStartsWithCapital,
|
|
68
|
+
validator.checkUniquePrimitiveAttributes,
|
|
69
|
+
],
|
|
70
|
+
Entity: [
|
|
71
|
+
validator.checkNameStartsWithCapital,
|
|
72
|
+
validator.checkUniqueAttributes,
|
|
73
|
+
],
|
|
74
|
+
DomainEvent: [
|
|
75
|
+
validator.checkNameStartsWithCapital,
|
|
76
|
+
validator.checkNameIsInPastTense,
|
|
77
|
+
],
|
|
78
|
+
DomainService: [
|
|
79
|
+
validator.checkNameStartsWithCapital,
|
|
80
|
+
validator.checkUniqueDtoAttributes,
|
|
81
|
+
],
|
|
82
|
+
Method: [
|
|
83
|
+
validator.checkUniqueDtoAttributes,
|
|
84
|
+
validator.checkNameIsCamelCase,
|
|
85
|
+
],
|
|
86
|
+
DomainException: validator.checkNameStartsWithCapital,
|
|
87
|
+
Payload: validator.checkUniquePrimitiveAttributes,
|
|
88
|
+
MethodsDeclaration: validator.checkUniqueMethods,
|
|
89
|
+
ExceptionsDeclaration: validator.checkUniqueExceptions,
|
|
90
|
+
DomainEventsDeclaration: validator.checkUniqueDomainEvents,
|
|
91
|
+
Port: validator.checkNameStartsWithCapital,
|
|
92
|
+
Command: [
|
|
93
|
+
validator.checkNameStartsWithCapital,
|
|
94
|
+
validator.checkUniqueDtoAttributes,
|
|
95
|
+
validator.checkUniqueCommandElements,
|
|
96
|
+
],
|
|
97
|
+
Enum: validator.checkUniqueValues,
|
|
98
|
+
EventStormingElement: validator.checkNameStartsWithCapital,
|
|
99
|
+
Policy: validator.checkNameStartsWithCapital,
|
|
100
|
+
ProcessFlow: [
|
|
101
|
+
validator.checkNameStartsWithCapital,
|
|
102
|
+
validator.checkUniquePolicies,
|
|
103
|
+
],
|
|
104
|
+
EventStormingDeclaration: validator.checkUniqueEventStormingElements,
|
|
105
|
+
ReactionStep: validator.checkUniqueOutcomes,
|
|
106
|
+
};
|
|
107
|
+
registry.register(checks, validator);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Implementation of custom validations.
|
|
112
|
+
*/
|
|
113
|
+
export class DomainDesignLanguageValidator {
|
|
114
|
+
checkUniquePurpose(model: DomainModel, accept: ValidationAcceptor): void {
|
|
115
|
+
if (model.boundedContext && model.contextMap)
|
|
116
|
+
accept(
|
|
117
|
+
'error',
|
|
118
|
+
'A file cannot contain both a Bounded Context and a Context Map. Please move the Context Map to a separate file.',
|
|
119
|
+
{
|
|
120
|
+
node: model.contextMap,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
checkUniqueTerms(
|
|
126
|
+
ubiquitousLanguage: UbiquitousLanguageDeclaration,
|
|
127
|
+
accept: ValidationAcceptor,
|
|
128
|
+
): void {
|
|
129
|
+
const terms = new Set<string>();
|
|
130
|
+
ubiquitousLanguage.terms.forEach((term) => {
|
|
131
|
+
if (terms.has(term.name))
|
|
132
|
+
accept(
|
|
133
|
+
'error',
|
|
134
|
+
`Repeated term name '${term.name}' in bounded context '${ubiquitousLanguage.$container.name}'.`,
|
|
135
|
+
{
|
|
136
|
+
node: term,
|
|
137
|
+
property: 'name',
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
terms.add(term.name);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
checkUniqueAggregates(
|
|
145
|
+
boundedContext: BoundedContext,
|
|
146
|
+
accept: ValidationAcceptor,
|
|
147
|
+
): void {
|
|
148
|
+
const aggregates = new Set<string>();
|
|
149
|
+
boundedContext.aggregates.forEach((aggregate) => {
|
|
150
|
+
if (aggregates.has(aggregate.name))
|
|
151
|
+
accept(
|
|
152
|
+
'error',
|
|
153
|
+
`Repeated aggregate root name '${aggregate.name}' in bounded context '${boundedContext.name}'.`,
|
|
154
|
+
{
|
|
155
|
+
node: aggregate,
|
|
156
|
+
property: 'name',
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
aggregates.add(aggregate.name);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
checkUniqueDomainServices(
|
|
164
|
+
boundedContext: BoundedContext,
|
|
165
|
+
accept: ValidationAcceptor,
|
|
166
|
+
): void {
|
|
167
|
+
const domainServices = new Set<string>();
|
|
168
|
+
boundedContext.domainServices.forEach((domainService) => {
|
|
169
|
+
if (domainServices.has(domainService.name))
|
|
170
|
+
accept(
|
|
171
|
+
'error',
|
|
172
|
+
`Repeated domain service name '${domainService.name}' in bounded context '${boundedContext.name}'.`,
|
|
173
|
+
{
|
|
174
|
+
node: domainService,
|
|
175
|
+
property: 'name',
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
domainServices.add(domainService.name);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
checkUniquePorts(
|
|
183
|
+
boundedContext: BoundedContext,
|
|
184
|
+
accept: ValidationAcceptor,
|
|
185
|
+
): void {
|
|
186
|
+
const ports = new Set<string>();
|
|
187
|
+
boundedContext.ports.forEach((port) => {
|
|
188
|
+
if (ports.has(port.name))
|
|
189
|
+
accept(
|
|
190
|
+
'error',
|
|
191
|
+
`Repeated port name '${port.name}' in bounded context '${boundedContext.name}'.`,
|
|
192
|
+
{
|
|
193
|
+
node: port,
|
|
194
|
+
property: 'name',
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
ports.add(port.name);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
checkUniqueCommands(
|
|
202
|
+
boundedContext: BoundedContext,
|
|
203
|
+
accept: ValidationAcceptor,
|
|
204
|
+
): void {
|
|
205
|
+
const commands = new Set<string>();
|
|
206
|
+
boundedContext.commands.forEach((command) => {
|
|
207
|
+
if (commands.has(command.name))
|
|
208
|
+
accept(
|
|
209
|
+
'error',
|
|
210
|
+
`Repeated command name '${command.name}' in bounded context '${boundedContext.name}'.`,
|
|
211
|
+
{
|
|
212
|
+
node: command,
|
|
213
|
+
property: 'name',
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
commands.add(command.name);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
checkUniqueEventStormingElements(
|
|
221
|
+
eventStormingDeclaration: EventStormingDeclaration,
|
|
222
|
+
accept: ValidationAcceptor,
|
|
223
|
+
): void {
|
|
224
|
+
const actors = new Set<string>();
|
|
225
|
+
const readModels = new Set<string>();
|
|
226
|
+
const externalSystems = new Set<string>();
|
|
227
|
+
eventStormingDeclaration.elements.forEach((element) => {
|
|
228
|
+
if (isActor(element)) {
|
|
229
|
+
if (actors.has(element.name))
|
|
230
|
+
accept(
|
|
231
|
+
'error',
|
|
232
|
+
`Repeated actor name '${element.name}' in event storming.`,
|
|
233
|
+
{
|
|
234
|
+
node: element,
|
|
235
|
+
property: 'name',
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
actors.add(element.name);
|
|
239
|
+
} else if (isReadModel(element)) {
|
|
240
|
+
if (readModels.has(element.name))
|
|
241
|
+
accept(
|
|
242
|
+
'error',
|
|
243
|
+
`Repeated read model name '${element.name}' in event storming.`,
|
|
244
|
+
{
|
|
245
|
+
node: element,
|
|
246
|
+
property: 'name',
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
readModels.add(element.name);
|
|
250
|
+
} else if (isExternalSystem(element)) {
|
|
251
|
+
if (externalSystems.has(element.name))
|
|
252
|
+
accept(
|
|
253
|
+
'error',
|
|
254
|
+
`Repeated external system name '${element.name}' in event storming.`,
|
|
255
|
+
{
|
|
256
|
+
node: element,
|
|
257
|
+
property: 'name',
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
externalSystems.add(element.name);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const processFlows = new Set<string>();
|
|
264
|
+
eventStormingDeclaration.flows.forEach((processFlow) => {
|
|
265
|
+
if (processFlows.has(processFlow.name))
|
|
266
|
+
accept(
|
|
267
|
+
'error',
|
|
268
|
+
`Repeated process flow name '${processFlow.name}' in event storming.`,
|
|
269
|
+
{
|
|
270
|
+
node: processFlow,
|
|
271
|
+
property: 'name',
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
processFlows.add(processFlow.name);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
checkUniquePolicies(
|
|
279
|
+
processFlow: ProcessFlow,
|
|
280
|
+
accept: ValidationAcceptor,
|
|
281
|
+
): void {
|
|
282
|
+
const policies = new Set<string>();
|
|
283
|
+
const outcomes: PolicyTrigger[] = processFlow.reactions.flatMap((step) =>
|
|
284
|
+
step.outcomes.filter(isPolicyTrigger),
|
|
285
|
+
);
|
|
286
|
+
outcomes.forEach((outcome) => {
|
|
287
|
+
if (outcome.policy.name && policies.has(outcome.policy.name))
|
|
288
|
+
accept(
|
|
289
|
+
'error',
|
|
290
|
+
`Repeated policy '${outcome.policy.name}' in process flow.`,
|
|
291
|
+
{
|
|
292
|
+
node: outcome,
|
|
293
|
+
property: 'policy',
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
policies.add(outcome.policy?.name || '');
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
checkUniqueOutcomes(step: ReactionStep, accept: ValidationAcceptor): void {
|
|
301
|
+
const readModels = new Set<string>();
|
|
302
|
+
const policies = new Set<string>();
|
|
303
|
+
step.outcomes.forEach((outcome) => {
|
|
304
|
+
if (isProjection(outcome)) {
|
|
305
|
+
if (
|
|
306
|
+
outcome.readModel.ref?.name &&
|
|
307
|
+
readModels.has(outcome.readModel.ref?.name)
|
|
308
|
+
)
|
|
309
|
+
accept(
|
|
310
|
+
'error',
|
|
311
|
+
`Repeated read model '${outcome.readModel.ref?.name}' projection in 'on' definition.`,
|
|
312
|
+
{
|
|
313
|
+
node: outcome,
|
|
314
|
+
property: 'readModel',
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
readModels.add(outcome.readModel?.ref?.name || '');
|
|
318
|
+
} else if (isPolicyTrigger(outcome)) {
|
|
319
|
+
if (outcome.policy.name && policies.has(outcome.policy.name))
|
|
320
|
+
accept(
|
|
321
|
+
'error',
|
|
322
|
+
`Repeated policy '${outcome.policy.name}' in 'on' definition.`,
|
|
323
|
+
{
|
|
324
|
+
node: outcome,
|
|
325
|
+
property: 'policy',
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
policies.add(outcome.policy?.name || '');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
checkUniqueDomainEvents(
|
|
334
|
+
declaration: DomainEventsDeclaration,
|
|
335
|
+
accept: ValidationAcceptor,
|
|
336
|
+
): void {
|
|
337
|
+
const names = new Set<string>();
|
|
338
|
+
declaration.events.forEach((event) => {
|
|
339
|
+
if (names.has(event.name)) {
|
|
340
|
+
accept(
|
|
341
|
+
'error',
|
|
342
|
+
`Repeated event name '${event.name}' in aggregate ${declaration.$container.name}.`,
|
|
343
|
+
{
|
|
344
|
+
node: event,
|
|
345
|
+
property: 'name',
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
names.add(event.name);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
checkUniqueMethods(
|
|
354
|
+
declaration: MethodsDeclaration,
|
|
355
|
+
accept: ValidationAcceptor,
|
|
356
|
+
): void {
|
|
357
|
+
const names = new Set<string>();
|
|
358
|
+
declaration.methods.forEach((method) => {
|
|
359
|
+
if (names.has(method.name)) {
|
|
360
|
+
accept(
|
|
361
|
+
'error',
|
|
362
|
+
`Repeated method name '${method.name}' in aggregate ${declaration.$container.name}.`,
|
|
363
|
+
{
|
|
364
|
+
node: method,
|
|
365
|
+
property: 'name',
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
names.add(method.name);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
checkUniqueExceptions(
|
|
374
|
+
declaration: ExceptionsDeclaration,
|
|
375
|
+
accept: ValidationAcceptor,
|
|
376
|
+
): void {
|
|
377
|
+
const names = new Set<string>();
|
|
378
|
+
declaration.exceptions.forEach((exception) => {
|
|
379
|
+
if (names.has(exception.name)) {
|
|
380
|
+
accept(
|
|
381
|
+
'error',
|
|
382
|
+
`Repeated exception name '${exception.name}' in aggregate ${declaration.$container.name}.`,
|
|
383
|
+
{
|
|
384
|
+
node: exception,
|
|
385
|
+
property: 'name',
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
names.add(exception.name);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
checkUniqueAttributes(
|
|
394
|
+
entity: Entity | AggregateRoot,
|
|
395
|
+
accept: ValidationAcceptor,
|
|
396
|
+
): void {
|
|
397
|
+
const attributes = new Set<string>();
|
|
398
|
+
entity.attributes.forEach((attribute) => {
|
|
399
|
+
if (attributes.has(attribute.type.name)) {
|
|
400
|
+
accept(
|
|
401
|
+
'error',
|
|
402
|
+
`Repeated attribute name '${attribute.type.name}' in ${
|
|
403
|
+
isAggregateRoot(entity) ? 'aggregate root' : 'entity'
|
|
404
|
+
} '${entity.name}'.`,
|
|
405
|
+
{
|
|
406
|
+
node: attribute.type,
|
|
407
|
+
property: 'name',
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
attributes.add(attribute.type.name);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
checkUniquePrimitiveAttributes(
|
|
416
|
+
node: ValueObject | Payload,
|
|
417
|
+
accept: ValidationAcceptor,
|
|
418
|
+
): void {
|
|
419
|
+
const attributes = new Set<string>();
|
|
420
|
+
node.attributes.forEach((attribute) => {
|
|
421
|
+
if (attributes.has(attribute.name)) {
|
|
422
|
+
accept(
|
|
423
|
+
'error',
|
|
424
|
+
`Repeated attribute name '${attribute.name}' in ${
|
|
425
|
+
isValueObject(node) ? 'value object' : 'payload'
|
|
426
|
+
}${isValueObject(node) ? ` '${node.name}'.` : '.'}`,
|
|
427
|
+
{
|
|
428
|
+
node: attribute,
|
|
429
|
+
property: 'name',
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
attributes.add(attribute.name);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
checkUniqueDtoAttributes(
|
|
438
|
+
service: DomainService | Command | Method,
|
|
439
|
+
accept: ValidationAcceptor,
|
|
440
|
+
): void {
|
|
441
|
+
let attributes = new Set<string>();
|
|
442
|
+
service.input?.attributes.forEach((input) => {
|
|
443
|
+
if (attributes.has(input.name)) {
|
|
444
|
+
accept(
|
|
445
|
+
'error',
|
|
446
|
+
`Repeated attribute name '${input.name}' in input object of ${isMethod(service) ? 'method' : 'service'} '${service.name}'.`,
|
|
447
|
+
{
|
|
448
|
+
node: input,
|
|
449
|
+
property: 'name',
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
attributes.add(input.name);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
checkUniqueCommandElements(
|
|
458
|
+
service: Command,
|
|
459
|
+
accept: ValidationAcceptor,
|
|
460
|
+
): void {
|
|
461
|
+
const events = new Set<string>();
|
|
462
|
+
service.events.forEach((event, index) => {
|
|
463
|
+
if (event.ref?.name && events.has(event.ref.name)) {
|
|
464
|
+
accept(
|
|
465
|
+
'error',
|
|
466
|
+
`Repeated event '${event.ref.name}' in emit declaration of command '${service.name}'.`,
|
|
467
|
+
{
|
|
468
|
+
node: service,
|
|
469
|
+
property: 'events',
|
|
470
|
+
index,
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
events.add(event.ref?.name ?? '');
|
|
475
|
+
});
|
|
476
|
+
const domainServices = new Set<string>();
|
|
477
|
+
service.domainServices.forEach((domainService, index) => {
|
|
478
|
+
if (
|
|
479
|
+
domainService.ref?.name &&
|
|
480
|
+
domainServices.has(domainService.ref.name)
|
|
481
|
+
) {
|
|
482
|
+
accept(
|
|
483
|
+
'error',
|
|
484
|
+
`Repeated domain service '${domainService.ref.name}' in use declaration of command '${service.name}'.`,
|
|
485
|
+
{
|
|
486
|
+
node: service,
|
|
487
|
+
property: 'domainServices',
|
|
488
|
+
index,
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
domainServices.add(domainService.ref?.name ?? '');
|
|
493
|
+
});
|
|
494
|
+
//const repositories = new Set<string>();
|
|
495
|
+
const ports = new Set<string>();
|
|
496
|
+
service.dependencies.forEach((dependency) => {
|
|
497
|
+
if (dependency.port.ref) {
|
|
498
|
+
if (ports.has(dependency.port.ref.name))
|
|
499
|
+
accept(
|
|
500
|
+
'error',
|
|
501
|
+
`Repeated port '${dependency.port.ref.name}' in dependencies of service '${service.name}'.`,
|
|
502
|
+
{
|
|
503
|
+
node: dependency,
|
|
504
|
+
property: 'port',
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
ports.add(dependency.port.ref.name);
|
|
508
|
+
}
|
|
509
|
+
/* if (isPortReferenceDependency(dependency)) {
|
|
510
|
+
if (dependency.port.ref) {
|
|
511
|
+
if (ports.has(dependency.port.ref.name))
|
|
512
|
+
accept(
|
|
513
|
+
'error',
|
|
514
|
+
`Repeated port '${dependency.port.ref.name}' in dependencies of service '${service.name}'.`,
|
|
515
|
+
{
|
|
516
|
+
node: dependency,
|
|
517
|
+
property: 'port',
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
ports.add(dependency.port.ref.name);
|
|
521
|
+
}
|
|
522
|
+
} */ /* else {
|
|
523
|
+
if (dependency.aggregate.ref) {
|
|
524
|
+
if (repositories.has(dependency.aggregate.ref.name))
|
|
525
|
+
accept(
|
|
526
|
+
'error',
|
|
527
|
+
`Repeated repository of aggregate root '${dependency.aggregate.ref.name}' in dependencies of service '${service.name}'.`,
|
|
528
|
+
{
|
|
529
|
+
node: dependency,
|
|
530
|
+
property: 'aggregate',
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
repositories.add(dependency.aggregate.ref.name);
|
|
534
|
+
}
|
|
535
|
+
} */
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
checkUniqueRelationships(
|
|
540
|
+
contextMap: ContextMap,
|
|
541
|
+
accept: ValidationAcceptor,
|
|
542
|
+
): void {
|
|
543
|
+
const registeredRelationships = new Set<string[]>();
|
|
544
|
+
contextMap.relationships.forEach((relationship) => {
|
|
545
|
+
let boundedContext1: string | undefined;
|
|
546
|
+
let boundedContext2: string | undefined;
|
|
547
|
+
if (isAsymmetricRelationship(relationship)) {
|
|
548
|
+
boundedContext1 = relationship.upstream?.ref?.name;
|
|
549
|
+
boundedContext2 = relationship.downstream?.ref?.name;
|
|
550
|
+
}
|
|
551
|
+
if (isSymmetricRelationship(relationship)) {
|
|
552
|
+
boundedContext1 = relationship.participant1?.ref?.name;
|
|
553
|
+
boundedContext2 = relationship.participant2?.ref?.name;
|
|
554
|
+
}
|
|
555
|
+
if (!boundedContext1 || !boundedContext2) return;
|
|
556
|
+
registeredRelationships.forEach((rel) => {
|
|
557
|
+
if (
|
|
558
|
+
(rel[0] === boundedContext1 && rel[1] === boundedContext2) ||
|
|
559
|
+
(rel[0] === boundedContext2 && rel[1] === boundedContext1)
|
|
560
|
+
) {
|
|
561
|
+
accept(
|
|
562
|
+
'error',
|
|
563
|
+
`Repeated relationship between '${boundedContext1}' and '${boundedContext2}' in context map.`,
|
|
564
|
+
{
|
|
565
|
+
node: relationship,
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
registeredRelationships.add([boundedContext1, boundedContext2]);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
checkSelfRelationships(
|
|
575
|
+
relationship: Relationship,
|
|
576
|
+
accept: ValidationAcceptor,
|
|
577
|
+
): void {
|
|
578
|
+
let boundedContext1: string | undefined;
|
|
579
|
+
let boundedContext2: string | undefined;
|
|
580
|
+
if (isAsymmetricRelationship(relationship)) {
|
|
581
|
+
boundedContext1 = relationship.upstream?.ref?.name;
|
|
582
|
+
boundedContext2 = relationship.downstream?.ref?.name;
|
|
583
|
+
}
|
|
584
|
+
if (isSymmetricRelationship(relationship)) {
|
|
585
|
+
boundedContext1 = relationship.participant1?.ref?.name;
|
|
586
|
+
boundedContext2 = relationship.participant2?.ref?.name;
|
|
587
|
+
}
|
|
588
|
+
if (boundedContext1 && boundedContext2) {
|
|
589
|
+
if (boundedContext1 === boundedContext2) {
|
|
590
|
+
accept('error', 'Self-referential relationships are not allowed.', {
|
|
591
|
+
node: relationship,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
checkRelationshipRoles(
|
|
598
|
+
relationship: AsymmetricRelationship,
|
|
599
|
+
accept: ValidationAcceptor,
|
|
600
|
+
): void {
|
|
601
|
+
if (relationship.customerSupplier) {
|
|
602
|
+
if (relationship.downstreamRole === 'Conformist')
|
|
603
|
+
accept(
|
|
604
|
+
'error',
|
|
605
|
+
'Cannot have a Conformist role in a customer/supplier relationship.',
|
|
606
|
+
{
|
|
607
|
+
node: relationship,
|
|
608
|
+
property: 'downstreamRole',
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
else if (relationship.downstreamRole === 'AntiCorruptionLayer')
|
|
612
|
+
accept(
|
|
613
|
+
'warning',
|
|
614
|
+
'Anti-Corruption Layer pattern should not be used in a customer/supplier relationship.',
|
|
615
|
+
{
|
|
616
|
+
node: relationship,
|
|
617
|
+
property: 'downstreamRole',
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
if (relationship.upstreamRoles.includes('OpenHostService')) {
|
|
621
|
+
accept(
|
|
622
|
+
'error',
|
|
623
|
+
'Cannot have an Open Host Service role in a customer/supplier relationship.',
|
|
624
|
+
{
|
|
625
|
+
node: relationship,
|
|
626
|
+
property: 'upstreamRoles',
|
|
627
|
+
index: relationship.upstreamRoles.indexOf('OpenHostService'),
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (
|
|
633
|
+
relationship.upstreamRoles?.[0] &&
|
|
634
|
+
relationship.upstreamRoles?.[0] === relationship.upstreamRoles?.[1]
|
|
635
|
+
) {
|
|
636
|
+
accept('error', 'Cannot have repeated roles.', {
|
|
637
|
+
node: relationship,
|
|
638
|
+
property: 'upstreamRoles',
|
|
639
|
+
index: 1,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
checkUniqueValues(enumDefinition: Enum, accept: ValidationAcceptor): void {
|
|
645
|
+
const literals = new Set<string>();
|
|
646
|
+
enumDefinition.values.forEach((value) => {
|
|
647
|
+
if (literals.has(value)) {
|
|
648
|
+
accept('error', `Duplicate value found: ${value}`, {
|
|
649
|
+
node: enumDefinition,
|
|
650
|
+
property: 'values',
|
|
651
|
+
index: Array.from(literals).indexOf(value),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
literals.add(value);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
checkNameIsInPastTense(event: DomainEvent, accept: ValidationAcceptor): void {
|
|
659
|
+
if (event.name) {
|
|
660
|
+
const lastTwoChars = event.name.slice(-2);
|
|
661
|
+
if (lastTwoChars !== 'ed') {
|
|
662
|
+
accept('hint', 'Check if the name is in past tense.', {
|
|
663
|
+
node: event,
|
|
664
|
+
property: 'name',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
checkNameStartsWithCapital(
|
|
671
|
+
node: AstNode & { name?: string },
|
|
672
|
+
accept: ValidationAcceptor,
|
|
673
|
+
): void {
|
|
674
|
+
if (node.name) {
|
|
675
|
+
const firstChar = node.name.substring(0, 1);
|
|
676
|
+
if (firstChar.toUpperCase() !== firstChar) {
|
|
677
|
+
accept('warning', 'The name should start with a capital letter.', {
|
|
678
|
+
node: node,
|
|
679
|
+
property: 'name',
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
checkNameIsCamelCase(
|
|
686
|
+
node: AstNode & { name?: string },
|
|
687
|
+
accept: ValidationAcceptor,
|
|
688
|
+
): void {
|
|
689
|
+
if (node.name) {
|
|
690
|
+
const firstChar = node.name.substring(0, 1);
|
|
691
|
+
if (firstChar.toLowerCase() !== firstChar) {
|
|
692
|
+
accept('warning', 'The name should start with a lowercase letter.', {
|
|
693
|
+
node: node,
|
|
694
|
+
property: 'name',
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|