From 06d20c203f1a45439301844532f24a1414c98d75 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Tue, 29 Apr 2025 12:04:25 +0200 Subject: [PATCH 1/9] add scope computation to prevent exporting scope elements to global scope --- src/language/ContextMapperDslModule.ts | 4 +++- .../ContextMapperDslScopeComputation.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/language/references/ContextMapperDslScopeComputation.ts diff --git a/src/language/ContextMapperDslModule.ts b/src/language/ContextMapperDslModule.ts index e1e6244..e8fa620 100644 --- a/src/language/ContextMapperDslModule.ts +++ b/src/language/ContextMapperDslModule.ts @@ -15,6 +15,7 @@ import { ContextMapperDslValidationRegistry } from './validation/ContextMapperDs import { ContextMapperValidationProviderRegistry } from './validation/ContextMapperValidationProviderRegistry.js' import { ContextMapperDslScopeProvider } from './references/ContextMapperDslScopeProvider.js' import { ContextMapperDslFoldingRangeProvider } from './folding/ContextMapperDslFoldingRageProvider.js' +import { ContextMapperDslScopeComputation } from './references/ContextMapperDslScopeComputation.js' /** * Declaration of custom services - add your own service classes here. @@ -52,7 +53,8 @@ export const ContextMapperDslModule: Module new ContextMapperDslFoldingRangeProvider(services) }, references: { - ScopeProvider: (services) => new ContextMapperDslScopeProvider(services) + ScopeProvider: (services) => new ContextMapperDslScopeProvider(services), + ScopeComputation: (services) => new ContextMapperDslScopeComputation(services) } } diff --git a/src/language/references/ContextMapperDslScopeComputation.ts b/src/language/references/ContextMapperDslScopeComputation.ts new file mode 100644 index 0000000..be92523 --- /dev/null +++ b/src/language/references/ContextMapperDslScopeComputation.ts @@ -0,0 +1,18 @@ +import { AstNode, AstNodeDescription, DefaultScopeComputation, LangiumDocument } from 'langium' +import { CancellationToken } from 'vscode-languageserver' + +export class ContextMapperDslScopeComputation extends DefaultScopeComputation { + /* + For the time being imports aren't supported yet. + By default, Langium adds top-level elements to the global scope. + Without this behavior is wrong and therefore no nodes must be exported here + */ + override computeExportsForNode ( + parentNode: AstNode, + document: LangiumDocument, + children?: (root: AstNode) => Iterable, + cancelToken?: CancellationToken + ): Promise { + return Promise.resolve([]) + } +} From 92a33c1ab136caaf9433c394071184317f9be99d Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Tue, 29 Apr 2025 12:04:48 +0200 Subject: [PATCH 2/9] add test to verify that cross-file references aren't included in autocomplete --- test/completion/Completion.test.ts | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/completion/Completion.test.ts diff --git a/test/completion/Completion.test.ts b/test/completion/Completion.test.ts new file mode 100644 index 0000000..ce89dec --- /dev/null +++ b/test/completion/Completion.test.ts @@ -0,0 +1,59 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { CompletionProvider } from 'langium/lsp' +import { clearDocuments, parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' +import { fail } from 'node:assert' + +let services: ReturnType +let completionProvider: CompletionProvider +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + completionProvider = services.ContextMapperDsl.lsp.CompletionProvider! + parse = parseHelper(services.ContextMapperDsl) +}) + +afterEach(async () => { + document && await clearDocuments(services.shared, [document]) +}) + +describe('Completion tests', () => { + test('completion should not include elements from other documents', async () => { + await parse(` + BoundedContext TestContext2 + `, { + validation: true + }) + await parse(` + BoundedContext TestContext3 + `) + + const docToComplete = await parse(` + ContextMap { + AnotherContext <-> T + } + BoundedContext TestContext + BoundedContext AnotherContext + `) + + const params = { + textDocument: { + uri: docToComplete.uri.path + }, + position: { + line: 2, + character: 28 + } + } + const completionList = await completionProvider.getCompletion(docToComplete, params) + if (completionList == null) { + fail('Expected completion provider to return completion list') + } + expect(completionList.items).toHaveLength(1) + expect(completionList.items[0].label).toEqual('TestContext') + }) +}) From 9279bc1bec667c8f2675d6984e6d85d429d8a993 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Thu, 1 May 2025 11:21:22 +0200 Subject: [PATCH 3/9] convert unordered group rules to (A|B|C)* rules to fix autocomplete --- src/language/ContextMapperDslModule.ts | 3 +- src/language/context-mapper-dsl.langium | 128 ++++---- .../ContextMapperDslValidationRegistry.ts | 17 +- .../validation/ContextMapperDslValidator.ts | 11 +- .../ContextMapperValidationProvider.ts | 1 - ...ContextMapperValidationProviderRegistry.ts | 51 +++- .../ContextMappingModelValidationProvider.ts | 22 -- src/language/validation/ValidationHelper.ts | 18 ++ .../validation/ValueValidationProvider.ts | 18 -- .../impl/AggregateValidationProvider.ts | 24 ++ .../impl/BoundedContextValidationProvider.ts | 20 ++ .../impl/ContextMapValidationProvider.ts | 11 + .../ContextMappingModelValidationProvider.ts | 10 + .../impl/SculptorModuleValidationProvider.ts | 12 + .../impl/StakeholderValidationProvider.ts | 12 + .../impl/SubDomainValidationProvider.ts | 10 + ...ownstreamRelationshipValidationProvider.ts | 12 + .../impl/UseCaseValidationProvider.ts | 17 ++ .../ValueElicitationValidationProvider.ts | 12 + .../impl/ValueEpicValidationProvider.ts | 10 + .../impl/ValueValidationProvider.ts | 10 + test/linking/AggregateLinking.test.ts | 6 +- test/linking/BoundedContextLinking.test.ts | 22 +- .../ContextMappingModelParsing.test.ts | 2 +- .../boundedContext/AggregateParsing.test.ts | 42 +-- .../BoundedContextParsing.test.ts | 42 +-- .../SculptorModuleParsing.test.ts | 12 +- .../contextMap/ContextMapParsing.test.ts | 24 +- .../contextMap/RelationshipParsing.test.ts | 38 +-- test/parsing/domain/DomainParsing.test.ts | 4 +- .../UserRequirementParsing.test.ts | 16 +- test/parsing/vdad/StakeholdersParsing.test.ts | 12 +- .../parsing/vdad/ValueRegisterParsing.test.ts | 8 +- test/validation/AggregateValidator.test.ts | 288 ++++++++++++++++++ .../BoundedContextValidator.test.ts | 179 +++++++++++ test/validation/ContextMapValidator.test.ts | 74 +++++ .../ContextMappingModelValidator.test.ts | 3 - .../SculptorModuleValidator.test.ts | 95 ++++++ test/validation/StakeholderValidator.test.ts | 95 ++++++ test/validation/SubDomainValidator.test.ts | 69 +++++ test/validation/UseCaseValidator.test.ts | 105 +++++++ .../ValueElicitationValidator.test.ts | 108 +++++++ test/validation/ValueValidator.test.ts | 3 - 43 files changed, 1428 insertions(+), 248 deletions(-) delete mode 100644 src/language/validation/ContextMappingModelValidationProvider.ts create mode 100644 src/language/validation/ValidationHelper.ts delete mode 100644 src/language/validation/ValueValidationProvider.ts create mode 100644 src/language/validation/impl/AggregateValidationProvider.ts create mode 100644 src/language/validation/impl/BoundedContextValidationProvider.ts create mode 100644 src/language/validation/impl/ContextMapValidationProvider.ts create mode 100644 src/language/validation/impl/ContextMappingModelValidationProvider.ts create mode 100644 src/language/validation/impl/SculptorModuleValidationProvider.ts create mode 100644 src/language/validation/impl/StakeholderValidationProvider.ts create mode 100644 src/language/validation/impl/SubDomainValidationProvider.ts create mode 100644 src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts create mode 100644 src/language/validation/impl/UseCaseValidationProvider.ts create mode 100644 src/language/validation/impl/ValueElicitationValidationProvider.ts create mode 100644 src/language/validation/impl/ValueEpicValidationProvider.ts create mode 100644 src/language/validation/impl/ValueValidationProvider.ts create mode 100644 test/validation/AggregateValidator.test.ts create mode 100644 test/validation/BoundedContextValidator.test.ts create mode 100644 test/validation/ContextMapValidator.test.ts create mode 100644 test/validation/SculptorModuleValidator.test.ts create mode 100644 test/validation/StakeholderValidator.test.ts create mode 100644 test/validation/SubDomainValidator.test.ts create mode 100644 test/validation/UseCaseValidator.test.ts create mode 100644 test/validation/ValueElicitationValidator.test.ts diff --git a/src/language/ContextMapperDslModule.ts b/src/language/ContextMapperDslModule.ts index e8fa620..e5559bb 100644 --- a/src/language/ContextMapperDslModule.ts +++ b/src/language/ContextMapperDslModule.ts @@ -16,6 +16,7 @@ import { ContextMapperValidationProviderRegistry } from './validation/ContextMap import { ContextMapperDslScopeProvider } from './references/ContextMapperDslScopeProvider.js' import { ContextMapperDslFoldingRangeProvider } from './folding/ContextMapperDslFoldingRageProvider.js' import { ContextMapperDslScopeComputation } from './references/ContextMapperDslScopeComputation.js' +import { ContextMapperDslCompletionProvider } from './completion/ContextMapperDslCompletionProvider.js' /** * Declaration of custom services - add your own service classes here. @@ -46,7 +47,7 @@ const validationProviderRegistry = new ContextMapperValidationProviderRegistry() export const ContextMapperDslModule: Module = { validation: { ContextMapperDslValidator: () => new ContextMapperDslValidator(validationProviderRegistry), - ValidationRegistry: (services) => new ContextMapperDslValidationRegistry(services) + ValidationRegistry: (services) => new ContextMapperDslValidationRegistry(services, validationProviderRegistry) }, lsp: { SemanticTokenProvider: (services) => new ContextMapperDslSemanticTokenProvider(services, semanticTokenProviderRegistry), diff --git a/src/language/context-mapper-dsl.langium b/src/language/context-mapper-dsl.langium index e6f73ce..c0d6e34 100644 --- a/src/language/context-mapper-dsl.langium +++ b/src/language/context-mapper-dsl.langium @@ -12,7 +12,7 @@ hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; entry ContextMappingModel: ( - (contextMaps+=ContextMap) | + (contextMap+=ContextMap) | (boundedContexts+=BoundedContext) | (domains+=Domain) | (userRequirements+=UserRequirement) | @@ -22,12 +22,10 @@ entry ContextMappingModel: ; /* - In Langium, elements in unordered groups are optional by default. - However, the usage of the ? cardinality is not allowed in unordered groups. - Therefore, to create a rule with optional, unordered elements, one needs to omit the ? operator and combine them with the & operator. - This behavior may change in the future. - Also, unordered groups may cause unreadable parsing errors. To resolve that, unordered groups can be replaced with a (A | B | C)* rule and enforce non-repetition of elements with a validator. + Unordered groups may cause unreadable parsing errors. To resolve that, unordered groups can be replaced with a (A | B | C)* rule and enforce non-repetition of elements with a validator. https://github.com/eclipse-langium/langium/discussions/1903 + Also, unordered groups cause issues with autocomplete. + Therefore all unordered group rules had to be converted to the recommended workaround from above. */ ContextMap: @@ -35,9 +33,9 @@ ContextMap: 'ContextMap' (name=ID)? OPEN ( - ('type' ('=')? type=ContextMapType) & - ('state' ('=')? state=ContextMapState) - ) + ('type' ('=')? type+=ContextMapType) | + ('state' ('=')? state+=ContextMapState) + )* ('contains' boundedContexts+=[BoundedContext] ("," boundedContexts+=[BoundedContext])*)* relationships+=Relationship* CLOSE @@ -46,22 +44,22 @@ ContextMap: BoundedContext: 'BoundedContext' name=ID ( ( - ('implements' (implementedDomainParts+=[DomainPart]) ("," implementedDomainParts+=[DomainPart])*) & - ('realizes' (realizedBoundedContexts+=[BoundedContext]) ("," realizedBoundedContexts+=[BoundedContext])*) & - ('refines' refinedBoundedContext=[BoundedContext]) - ) + ('implements' (implementedDomainParts+=[DomainPart]) ("," implementedDomainParts+=[DomainPart])*) | + ('realizes' (realizedBoundedContexts+=[BoundedContext]) ("," realizedBoundedContexts+=[BoundedContext])*) | + ('refines' refinedBoundedContext+=[BoundedContext]) + )* ) ( OPEN ( - ('domainVisionStatement' ('=')? domainVisionStatement=STRING) & - ('type' ('=')? type=BoundedContextType) & - (('responsibilities' ('=')? responsibilities+=STRING) ("," responsibilities+=STRING)*) & - ('implementationTechnology' ('=')? implementationTechnology=STRING) & - ('knowledgeLevel' ('=')? knowledgeLevel=KnowledgeLevel) & - ('businessModel' ('=')? businessModel=STRING) & - ('evolution' ('=')? evolution=Evolution) - ) + ('domainVisionStatement' ('=')? domainVisionStatement+=STRING) | + ('type' ('=')? type+=BoundedContextType) | + (('responsibilities' ('=')? responsibilities+=STRING) ("," responsibilities+=STRING)*) | + ('implementationTechnology' ('=')? implementationTechnology+=STRING) | + ('knowledgeLevel' ('=')? knowledgeLevel+=KnowledgeLevel) | + ('businessModel' ('=')? businessModel+=STRING) | + ('evolution' ('=')? evolution+=Evolution) + )* ( ( modules+=SculptorModule | @@ -91,9 +89,9 @@ Subdomain: ( OPEN ( - ('type' ('=')? type=SubDomainType) & - ('domainVisionStatement' ('=')? domainVisionStatement=STRING) - ) + ('type' ('=')? type+=SubDomainType) | + ('domainVisionStatement' ('=')? domainVisionStatement+=STRING) + )* CLOSE )? ; @@ -147,10 +145,10 @@ UpstreamDownstreamRelationship: (':' name=ID)? (OPEN ( - ('implementationTechnology' ('=')? implementationTechnology=STRING) & - (('exposedAggregates' ('=')? upstreamExposedAggregates+=[Aggregate]) ("," upstreamExposedAggregates+=[Aggregate])*) & - ('downstreamRights' ('=')? downstreamGovernanceRights=DownstreamGovernanceRights) - ) + ('implementationTechnology' ('=')? implementationTechnology+=STRING) | + (('exposedAggregates' ('=')? upstreamExposedAggregates+=[Aggregate]) ("," upstreamExposedAggregates+=[Aggregate])*) | + ('downstreamRights' ('=')? downstreamGovernanceRights+=DownstreamGovernanceRights) + )* CLOSE)? ) ; @@ -166,10 +164,10 @@ CustomerSupplierRelationship: (':' name=ID)? (OPEN ( - ('implementationTechnology' ('=')? implementationTechnology=STRING) & - (('exposedAggregates' ('=')? upstreamExposedAggregates+=[Aggregate]) ("," upstreamExposedAggregates+=[Aggregate])*) & - ('downstreamRights' ('=')? downstreamGovernanceRights=DownstreamGovernanceRights) - ) + ('implementationTechnology' ('=')? implementationTechnology+=STRING) | + (('exposedAggregates' ('=')? upstreamExposedAggregates+=[Aggregate]) ("," upstreamExposedAggregates+=[Aggregate])*) | + ('downstreamRights' ('=')? downstreamGovernanceRights+=DownstreamGovernanceRights) + )* CLOSE)? ) ; @@ -178,23 +176,23 @@ Aggregate: (doc=STRING)? "Aggregate" name=ID (OPEN ( - (('responsibilities' ('=')? responsibilities+=STRING) ("," responsibilities+=STRING)*) & + (('responsibilities' ('=')? responsibilities+=STRING) ("," responsibilities+=STRING)*) | ( (('useCases' ('=')? useCases+=[UseCase]) ("," useCases+=[UseCase])*) | (('userStories' ('=')? userStories+=[UserStory]) ("," userStories+=[UserStory])*) | ((('features' | 'userRequirements') ('=')? userRequirements+=[UserRequirement]) ("," userRequirements+=[UserRequirement])*) - ) & - ('owner' ('=')? owner=[BoundedContext]) & - ('knowledgeLevel' ('=')? knowledgeLevel=KnowledgeLevel) & - (('likelihoodForChange' | 'structuralVolatility') ('=')? likelihoodForChange=Volatility) & - ('contentVolatility' ('=')? contentVolatility=Volatility) & - ('availabilityCriticality' ('=')? availabilityCriticality=Criticality) & - ('consistencyCriticality' ('=')? consistencyCriticality=Criticality) & - ('storageSimilarity' ('=')? storageSimilarity=Similarity) & - ('securityCriticality' ('=')? securityCriticality=Criticality) & - ('securityZone' ('=')? securityZone=STRING) & - ('securityAccessGroup' ('=')? securityAccessGroup=STRING) - ) + ) | + ('owner' ('=')? owner+=[BoundedContext]) | + ('knowledgeLevel' ('=')? knowledgeLevel+=KnowledgeLevel) | + (('likelihoodForChange' | 'structuralVolatility') ('=')? likelihoodForChange+=Volatility) | + ('contentVolatility' ('=')? contentVolatility+=Volatility) | + ('availabilityCriticality' ('=')? availabilityCriticality+=Criticality) | + ('consistencyCriticality' ('=')? consistencyCriticality+=Criticality) | + ('storageSimilarity' ('=')? storageSimilarity+=Similarity) | + ('securityCriticality' ('=')? securityCriticality+=Criticality) | + ('securityZone' ('=')? securityZone+=STRING) | + ('securityAccessGroup' ('=')? securityAccessGroup+=STRING) + )* CLOSE)? ; @@ -205,13 +203,13 @@ UserRequirement: UseCase: 'UseCase' name=ID (OPEN ( - ('actor' ('=')? role=STRING) & - ('secondaryActors' ('=')? secondaryActors+=STRING ("," secondaryActors+=STRING)*) & - ('interactions' ('=')? features+=Feature ("," features+=Feature)*) & - ('benefit' ('=')? benefit=STRING) & - ('scope' ('=')? scope=STRING) & - ('level' ('=')? level=STRING) - ) + ('actor' ('=')? role+=STRING) | + ('secondaryActors' ('=')? secondaryActors+=STRING ("," secondaryActors+=STRING)*) | + ('interactions' ('=')? features+=Feature ("," features+=Feature)*) | + ('benefit' ('=')? benefit+=STRING) | + ('scope' ('=')? scope+=STRING) | + ('level' ('=')? level+=STRING) + )* CLOSE)? ; @@ -247,10 +245,10 @@ SculptorModule: 'Module' name=ID ( OPEN ( - (external?='external') & - ('basePackage' '=' basePackage=JavaIdentifier) & - ('hint' '=' hint=STRING) - ) + (external+='external') | + ('basePackage' '=' basePackage+=JavaIdentifier) | + ('hint' '=' hint+=STRING) + )* ( (aggregates+=Aggregate) )* @@ -286,10 +284,10 @@ StakeholderGroup: Stakeholder: 'Stakeholder' name=ID (OPEN ( - ('influence' ('=')? influence=INFLUENCE) & - ('interest' ('=')? interest=INTEREST) & - ('description' ('=')? description=STRING) - ) + ('influence' ('=')? influence+=INFLUENCE) | + ('interest' ('=')? interest+=INTEREST) | + ('description' ('=')? description+=STRING) + )* CLOSE)? ; @@ -334,10 +332,10 @@ Value: ValueElicitation: ('Stakeholder'|'Stakeholders') stakeholder=[AbstractStakeholder] (OPEN ( - ('priority' ('=')? priority=PRIORITY) & - ('impact' ('=')? impact=IMPACT) & + ('priority' ('=')? priority+=PRIORITY) | + ('impact' ('=')? impact+=IMPACT) | ('consequences' (consequences+=Consequence)+) - ) + )* CLOSE)? ; @@ -347,9 +345,9 @@ ValueEpic: ( 'As a' stakeholder=[AbstractStakeholder] 'I value' value=STRING 'as demonstrated in' ( - ('realization of' realizedValues+=STRING)+ & + ('realization of' realizedValues+=STRING)+ | ('reduction of' reducedValues+=STRING)+ - ) + )* ) CLOSE)? ; diff --git a/src/language/validation/ContextMapperDslValidationRegistry.ts b/src/language/validation/ContextMapperDslValidationRegistry.ts index 28b93ef..7969397 100644 --- a/src/language/validation/ContextMapperDslValidationRegistry.ts +++ b/src/language/validation/ContextMapperDslValidationRegistry.ts @@ -1,15 +1,20 @@ import { type ValidationChecks, ValidationRegistry } from 'langium' import { ContextMapperDslServices } from '../ContextMapperDslModule.js' -import type { ContextMapperDslAstType } from '../generated/ast.js' +import { ContextMapperDslAstType } from '../generated/ast.js' +import { ContextMapperValidationProviderRegistry } from './ContextMapperValidationProviderRegistry.js' export class ContextMapperDslValidationRegistry extends ValidationRegistry { - constructor (services: ContextMapperDslServices) { + constructor (services: ContextMapperDslServices, validationProviderRegistry: ContextMapperValidationProviderRegistry) { super(services) const validator = services.validation.ContextMapperDslValidator - const checks: ValidationChecks = { - ContextMappingModel: validator.checkContextMappingModel, - Value: validator.checkValue - } + + const typesToValidate = validationProviderRegistry.getRegisteredTypes() + + // dynamically set validator for all grammar elements + const checks: ValidationChecks = Object.fromEntries( + typesToValidate.map(type => [type, validator.validate]) + ) + super.register(checks, validator) } } diff --git a/src/language/validation/ContextMapperDslValidator.ts b/src/language/validation/ContextMapperDslValidator.ts index ad54a11..9139d3d 100644 --- a/src/language/validation/ContextMapperDslValidator.ts +++ b/src/language/validation/ContextMapperDslValidator.ts @@ -1,5 +1,4 @@ -import { ValidationAcceptor } from 'langium' -import type { ContextMappingModel, Value } from '../generated/ast.js' +import { AstNode, ValidationAcceptor } from 'langium' import { ContextMapperValidationProviderRegistry } from './ContextMapperValidationProviderRegistry.js' export class ContextMapperDslValidator { @@ -9,11 +8,7 @@ export class ContextMapperDslValidator { this._registry = contextMapperValidationProviderRegistry } - checkContextMappingModel (model: ContextMappingModel, acceptor: ValidationAcceptor) { - this._registry.get(model)?.validate(model, acceptor) - } - - checkValue (value: Value, acceptor: ValidationAcceptor) { - this._registry.get(value)?.validate(value, acceptor) + validate (node: AstNode, acceptor: ValidationAcceptor) { + this._registry.get(node)?.validate(node, acceptor) } } diff --git a/src/language/validation/ContextMapperValidationProvider.ts b/src/language/validation/ContextMapperValidationProvider.ts index e6ef6e3..6e81cb9 100644 --- a/src/language/validation/ContextMapperValidationProvider.ts +++ b/src/language/validation/ContextMapperValidationProvider.ts @@ -1,6 +1,5 @@ import { AstNode, ValidationAcceptor } from 'langium' export interface ContextMapperValidationProvider { - supports(node: AstNode): node is T validate (node: T, acceptor: ValidationAcceptor): void } diff --git a/src/language/validation/ContextMapperValidationProviderRegistry.ts b/src/language/validation/ContextMapperValidationProviderRegistry.ts index a3a5a58..576550f 100644 --- a/src/language/validation/ContextMapperValidationProviderRegistry.ts +++ b/src/language/validation/ContextMapperValidationProviderRegistry.ts @@ -1,15 +1,52 @@ import { AstNode } from 'langium' import { ContextMapperValidationProvider } from './ContextMapperValidationProvider.js' -import { ValueValidationProvider } from './ValueValidationProvider.js' -import { ContextMappingModelValidationProvider } from './ContextMappingModelValidationProvider.js' +import { ValueValidationProvider } from './impl/ValueValidationProvider.js' +import { ContextMappingModelValidationProvider } from './impl/ContextMappingModelValidationProvider.js' +import { ContextMapValidationProvider } from './impl/ContextMapValidationProvider.js' +import { + Aggregate, + BoundedContext, + ContextMap, + ContextMappingModel, + SculptorModule, + Stakeholder, + Subdomain, + UpstreamDownstreamRelationship, + UseCase, + Value, + ValueElicitation +} from '../generated/ast.js' +import { BoundedContextValidationProvider } from './impl/BoundedContextValidationProvider.js' +import { SubDomainValidationProvider } from './impl/SubDomainValidationProvider.js' +import { + UpstreamDownstreamRelationshipValidationProvider +} from './impl/UpstreamDownstreamRelationshipValidationProvider.js' +import { AggregateValidationProvider } from './impl/AggregateValidationProvider.js' +import { UseCaseValidationProvider } from './impl/UseCaseValidationProvider.js' +import { SculptorModuleValidationProvider } from './impl/SculptorModuleValidationProvider.js' +import { StakeholderValidationProvider } from './impl/StakeholderValidationProvider.js' +import { ValueElicitationValidationProvider } from './impl/ValueElicitationValidationProvider.js' export class ContextMapperValidationProviderRegistry { - private readonly _providers: ContextMapperValidationProvider[] = [ - new ValueValidationProvider(), - new ContextMappingModelValidationProvider() - ] + private readonly _providers = new Map>([ + [Value, new ValueValidationProvider()], + [ContextMappingModel, new ContextMappingModelValidationProvider()], + [ContextMap, new ContextMapValidationProvider()], + [BoundedContext, new BoundedContextValidationProvider()], + [Subdomain, new SubDomainValidationProvider()], + [UpstreamDownstreamRelationship, new UpstreamDownstreamRelationshipValidationProvider()], + [Aggregate, new AggregateValidationProvider()], + [UseCase, new UseCaseValidationProvider()], + [SculptorModule, new SculptorModuleValidationProvider()], + [Stakeholder, new StakeholderValidationProvider()], + [ValueElicitation, new ValueElicitationValidationProvider()] + ]) get (node: AstNode): ContextMapperValidationProvider | undefined { - return this._providers.find(p => p.supports(node)) + return this._providers.get(node.$type) + } + + getRegisteredTypes (): string[] { + return Array.from(this._providers.keys()) } } diff --git a/src/language/validation/ContextMappingModelValidationProvider.ts b/src/language/validation/ContextMappingModelValidationProvider.ts deleted file mode 100644 index 18e566b..0000000 --- a/src/language/validation/ContextMappingModelValidationProvider.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { AstNode, ValidationAcceptor } from 'langium' -import { ContextMappingModel, isContextMappingModel } from '../generated/ast.js' -import { ContextMapperValidationProvider } from './ContextMapperValidationProvider.js' - -export class ContextMappingModelValidationProvider implements ContextMapperValidationProvider { - supports (node: AstNode): node is ContextMappingModel { - return isContextMappingModel(node) - } - - validate (model: ContextMappingModel, acceptor: ValidationAcceptor): void { - this.checkForZeroOrOneContextMap(model, acceptor) - } - - private checkForZeroOrOneContextMap (model: ContextMappingModel, acceptor: ValidationAcceptor): void { - if (model.contextMaps.length > 1) { - acceptor('error', 'There must be zero or one context map', { - node: model, - property: 'contextMaps' - }) - } - } -} diff --git a/src/language/validation/ValidationHelper.ts b/src/language/validation/ValidationHelper.ts new file mode 100644 index 0000000..ba85dab --- /dev/null +++ b/src/language/validation/ValidationHelper.ts @@ -0,0 +1,18 @@ +import { AstNode, ValidationAcceptor } from 'langium' + +export function enforceZeroOrOneCardinality (node: AstNode, property: string, acceptor: ValidationAcceptor, propertyName: string = property) { + const nodeProperty = node[property as keyof AstNode] + if (!Array.isArray(nodeProperty)) { + acceptor('warning', `There was a problem validating the element ${propertyName}.`, { + node, + property + }) + return + } + if (nodeProperty != null && nodeProperty.length > 1) { + acceptor('error', `There must be zero or one ${propertyName} attribute`, { + node, + property + }) + } +} diff --git a/src/language/validation/ValueValidationProvider.ts b/src/language/validation/ValueValidationProvider.ts deleted file mode 100644 index 5590d42..0000000 --- a/src/language/validation/ValueValidationProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ContextMapperValidationProvider } from './ContextMapperValidationProvider.js' -import { isValue, Value } from '../generated/ast.js' -import { AstNode, ValidationAcceptor } from 'langium' - -export class ValueValidationProvider implements ContextMapperValidationProvider { - supports (node: AstNode): node is Value { - return isValue(node) - } - - validate (node: Value, acceptor: ValidationAcceptor): void { - if (node.coreValue.length > 1) { - acceptor('error', 'There must be zero or one isCore attribute', { - node, - property: 'coreValue' - }) - } - } -} diff --git a/src/language/validation/impl/AggregateValidationProvider.ts b/src/language/validation/impl/AggregateValidationProvider.ts new file mode 100644 index 0000000..84f7ee5 --- /dev/null +++ b/src/language/validation/impl/AggregateValidationProvider.ts @@ -0,0 +1,24 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { Aggregate } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class AggregateValidationProvider implements ContextMapperValidationProvider { + validate (node: Aggregate, acceptor: ValidationAcceptor): void { + // TODO: regex enforce responsibilities + // TODO: regex enforce useCases + // TODO: regex enforce userStories + // TODO: regex enforce userRequirements & features + + enforceZeroOrOneCardinality(node, 'owner', acceptor) + enforceZeroOrOneCardinality(node, 'knowledgeLevel', acceptor) + enforceZeroOrOneCardinality(node, 'likelihoodForChange', acceptor) + enforceZeroOrOneCardinality(node, 'contentVolatility', acceptor) + enforceZeroOrOneCardinality(node, 'availabilityCriticality', acceptor) + enforceZeroOrOneCardinality(node, 'consistencyCriticality', acceptor) + enforceZeroOrOneCardinality(node, 'storageSimilarity', acceptor) + enforceZeroOrOneCardinality(node, 'securityCriticality', acceptor) + enforceZeroOrOneCardinality(node, 'securityZone', acceptor) + enforceZeroOrOneCardinality(node, 'securityAccessGroup', acceptor) + } +} diff --git a/src/language/validation/impl/BoundedContextValidationProvider.ts b/src/language/validation/impl/BoundedContextValidationProvider.ts new file mode 100644 index 0000000..4fa1151 --- /dev/null +++ b/src/language/validation/impl/BoundedContextValidationProvider.ts @@ -0,0 +1,20 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { BoundedContext } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class BoundedContextValidationProvider implements ContextMapperValidationProvider { + validate (node: BoundedContext, acceptor: ValidationAcceptor): void { + // TODO: regex enforce implementedDomainParts + // TODO: regex enforce realizedBoundedContexts + enforceZeroOrOneCardinality(node, 'refinedBoundedContext', acceptor, 'refines') + + enforceZeroOrOneCardinality(node, 'domainVisionStatement', acceptor) + enforceZeroOrOneCardinality(node, 'type', acceptor) + // TODO: regex enforce responsibilities + enforceZeroOrOneCardinality(node, 'implementationTechnology', acceptor) + enforceZeroOrOneCardinality(node, 'knowledgeLevel', acceptor) + enforceZeroOrOneCardinality(node, 'businessModel', acceptor) + enforceZeroOrOneCardinality(node, 'evolution', acceptor) + } +} diff --git a/src/language/validation/impl/ContextMapValidationProvider.ts b/src/language/validation/impl/ContextMapValidationProvider.ts new file mode 100644 index 0000000..305fb29 --- /dev/null +++ b/src/language/validation/impl/ContextMapValidationProvider.ts @@ -0,0 +1,11 @@ +import { ValidationAcceptor } from 'langium' +import { ContextMap } from '../../generated/ast.js' +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class ContextMapValidationProvider implements ContextMapperValidationProvider { + validate (node: ContextMap, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'type', acceptor) + enforceZeroOrOneCardinality(node, 'state', acceptor) + } +} diff --git a/src/language/validation/impl/ContextMappingModelValidationProvider.ts b/src/language/validation/impl/ContextMappingModelValidationProvider.ts new file mode 100644 index 0000000..ecccb41 --- /dev/null +++ b/src/language/validation/impl/ContextMappingModelValidationProvider.ts @@ -0,0 +1,10 @@ +import type { ValidationAcceptor } from 'langium' +import { ContextMappingModel } from '../../generated/ast.js' +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class ContextMappingModelValidationProvider implements ContextMapperValidationProvider { + validate (model: ContextMappingModel, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(model, 'contextMap', acceptor) + } +} diff --git a/src/language/validation/impl/SculptorModuleValidationProvider.ts b/src/language/validation/impl/SculptorModuleValidationProvider.ts new file mode 100644 index 0000000..35f9590 --- /dev/null +++ b/src/language/validation/impl/SculptorModuleValidationProvider.ts @@ -0,0 +1,12 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { SculptorModule } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class SculptorModuleValidationProvider implements ContextMapperValidationProvider { + validate (node: SculptorModule, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'external', acceptor) + enforceZeroOrOneCardinality(node, 'basePackage', acceptor) + enforceZeroOrOneCardinality(node, 'hint', acceptor) + } +} diff --git a/src/language/validation/impl/StakeholderValidationProvider.ts b/src/language/validation/impl/StakeholderValidationProvider.ts new file mode 100644 index 0000000..93d6433 --- /dev/null +++ b/src/language/validation/impl/StakeholderValidationProvider.ts @@ -0,0 +1,12 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { Stakeholder } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class StakeholderValidationProvider implements ContextMapperValidationProvider { + validate (node: Stakeholder, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'influence', acceptor) + enforceZeroOrOneCardinality(node, 'interest', acceptor) + enforceZeroOrOneCardinality(node, 'description', acceptor) + } +} diff --git a/src/language/validation/impl/SubDomainValidationProvider.ts b/src/language/validation/impl/SubDomainValidationProvider.ts new file mode 100644 index 0000000..208bac3 --- /dev/null +++ b/src/language/validation/impl/SubDomainValidationProvider.ts @@ -0,0 +1,10 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { Subdomain } from '../../generated/ast.js' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class SubDomainValidationProvider implements ContextMapperValidationProvider { + validate (node: Subdomain, acceptor: any): void { + enforceZeroOrOneCardinality(node, 'type', acceptor) + enforceZeroOrOneCardinality(node, 'domainVisionStatement', acceptor) + } +} diff --git a/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts b/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts new file mode 100644 index 0000000..a3293f5 --- /dev/null +++ b/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts @@ -0,0 +1,12 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { UpstreamDownstreamRelationship } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class UpstreamDownstreamRelationshipValidationProvider implements ContextMapperValidationProvider { + validate (node: UpstreamDownstreamRelationship, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'implementationTechnology', acceptor) + // TODO: regex enforce exposedAggregates + enforceZeroOrOneCardinality(node, 'downstreamRights', acceptor) + } +} diff --git a/src/language/validation/impl/UseCaseValidationProvider.ts b/src/language/validation/impl/UseCaseValidationProvider.ts new file mode 100644 index 0000000..9fe35eb --- /dev/null +++ b/src/language/validation/impl/UseCaseValidationProvider.ts @@ -0,0 +1,17 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { isUseCase, UserRequirement } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class UseCaseValidationProvider implements ContextMapperValidationProvider { + validate (node: UserRequirement, acceptor: ValidationAcceptor): void { + if (isUseCase(node)) { + enforceZeroOrOneCardinality(node, 'role', acceptor, 'actor') + // TODO: regex enforce secondaryActors + // TODO: regex enforce features + enforceZeroOrOneCardinality(node, 'benefit', acceptor) + enforceZeroOrOneCardinality(node, 'scope', acceptor) + enforceZeroOrOneCardinality(node, 'level', acceptor) + } + } +} diff --git a/src/language/validation/impl/ValueElicitationValidationProvider.ts b/src/language/validation/impl/ValueElicitationValidationProvider.ts new file mode 100644 index 0000000..8b60223 --- /dev/null +++ b/src/language/validation/impl/ValueElicitationValidationProvider.ts @@ -0,0 +1,12 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { ValueElicitation } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class ValueElicitationValidationProvider implements ContextMapperValidationProvider { + validate (node: ValueElicitation, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'priority', acceptor) + enforceZeroOrOneCardinality(node, 'impact', acceptor) + // TODO: regex enforce consequences + } +} diff --git a/src/language/validation/impl/ValueEpicValidationProvider.ts b/src/language/validation/impl/ValueEpicValidationProvider.ts new file mode 100644 index 0000000..40cccb8 --- /dev/null +++ b/src/language/validation/impl/ValueEpicValidationProvider.ts @@ -0,0 +1,10 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { ValueEpic } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' + +export class ValueEpicValidationProvider implements ContextMapperValidationProvider { + validate (node: ValueEpic, acceptor: ValidationAcceptor): void { + // TODO: regex enforce realizedValues + // TODO: regex enforce reducedValues + } +} diff --git a/src/language/validation/impl/ValueValidationProvider.ts b/src/language/validation/impl/ValueValidationProvider.ts new file mode 100644 index 0000000..b09be51 --- /dev/null +++ b/src/language/validation/impl/ValueValidationProvider.ts @@ -0,0 +1,10 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { Value } from '../../generated/ast.js' +import { ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' + +export class ValueValidationProvider implements ContextMapperValidationProvider { + validate (node: Value, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'coreValue', acceptor, 'isCore') + } +} diff --git a/test/linking/AggregateLinking.test.ts b/test/linking/AggregateLinking.test.ts index ee63e50..dd67307 100644 --- a/test/linking/AggregateLinking.test.ts +++ b/test/linking/AggregateLinking.test.ts @@ -36,7 +36,7 @@ describe('Aggregate linking tests', () => { } `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as UpstreamDownstreamRelationship + const relationship = document.parseResult.value.contextMap[0].relationships[0] as UpstreamDownstreamRelationship expect(relationship.upstreamExposedAggregates).toHaveLength(1) expect(relationship.upstreamExposedAggregates[0]).not.toBeUndefined() expect(relationship.upstreamExposedAggregates[0].ref).not.toBeUndefined() @@ -56,7 +56,7 @@ describe('Aggregate linking tests', () => { } `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as CustomerSupplierRelationship + const relationship = document.parseResult.value.contextMap[0].relationships[0] as CustomerSupplierRelationship expect(relationship.upstreamExposedAggregates).toHaveLength(1) expect(relationship.upstreamExposedAggregates[0]).not.toBeUndefined() expect(relationship.upstreamExposedAggregates[0].ref).not.toBeUndefined() @@ -78,7 +78,7 @@ describe('Aggregate linking tests', () => { } `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as CustomerSupplierRelationship + const relationship = document.parseResult.value.contextMap[0].relationships[0] as CustomerSupplierRelationship expect(relationship.upstreamExposedAggregates).toHaveLength(1) expect(relationship.upstreamExposedAggregates[0]).not.toBeUndefined() expect(relationship.upstreamExposedAggregates[0].ref).not.toBeUndefined() diff --git a/test/linking/BoundedContextLinking.test.ts b/test/linking/BoundedContextLinking.test.ts index 2ace57b..b98d9ec 100644 --- a/test/linking/BoundedContextLinking.test.ts +++ b/test/linking/BoundedContextLinking.test.ts @@ -34,7 +34,7 @@ describe('Bounded context linking tests', () => { BoundedContext TestContext `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as SharedKernel + const relationship = document.parseResult.value.contextMap[0].relationships[0] as SharedKernel expect(relationship.participant1.ref).not.toBeUndefined() expect(relationship.participant1.ref?.name).toEqual('TestContext') expect(relationship.participant2.ref).not.toBeUndefined() @@ -50,7 +50,7 @@ describe('Bounded context linking tests', () => { BoundedContext TestContext `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as Partnership + const relationship = document.parseResult.value.contextMap[0].relationships[0] as Partnership expect(relationship.participant1.ref).not.toBeUndefined() expect(relationship.participant1.ref?.name).toEqual('FirstContext') expect(relationship.participant2.ref).not.toBeUndefined() @@ -66,7 +66,7 @@ describe('Bounded context linking tests', () => { BoundedContext TestContext `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as UpstreamDownstreamRelationship + const relationship = document.parseResult.value.contextMap[0].relationships[0] as UpstreamDownstreamRelationship expect(relationship.upstream.ref).not.toBeUndefined() expect(relationship.upstream.ref?.name).toEqual('FirstContext') expect(relationship.downstream.ref).not.toBeUndefined() @@ -82,7 +82,7 @@ describe('Bounded context linking tests', () => { BoundedContext TestContext `) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as CustomerSupplierRelationship + const relationship = document.parseResult.value.contextMap[0].relationships[0] as CustomerSupplierRelationship expect(relationship.downstream.ref).not.toBeUndefined() expect(relationship.downstream.ref?.name).toEqual('FirstContext') expect(relationship.upstream.ref).not.toBeUndefined() @@ -99,7 +99,7 @@ describe('Bounded context linking tests', () => { BoundedContext SecondContext `) - const contextMap = document.parseResult.value.contextMaps[0] + const contextMap = document.parseResult.value.contextMap[0] expect(contextMap.boundedContexts).toHaveLength(2) expect(contextMap.boundedContexts[0].ref).not.toBeUndefined() expect(contextMap.boundedContexts[0].ref?.name).toEqual('FirstContext') @@ -118,9 +118,9 @@ describe('Bounded context linking tests', () => { `) const boundedContext = document.parseResult.value.boundedContexts[0] - expect(boundedContext.refinedBoundedContext).not.toBeUndefined() - expect(boundedContext.refinedBoundedContext.ref).not.toBeUndefined() - expect(boundedContext.refinedBoundedContext.ref?.name).toEqual('RefinedContext') + expect(boundedContext.refinedBoundedContext).toHaveLength(1) + expect(boundedContext.refinedBoundedContext[0].ref).not.toBeUndefined() + expect(boundedContext.refinedBoundedContext[0].ref?.name).toEqual('RefinedContext') expect(boundedContext.realizedBoundedContexts).toHaveLength(1) expect(boundedContext.realizedBoundedContexts[0].ref).not.toBeUndefined() expect(boundedContext.realizedBoundedContexts[0].ref?.name).toEqual('RealizedContext') @@ -136,9 +136,9 @@ describe('Bounded context linking tests', () => { `) const aggregate = document.parseResult.value.boundedContexts[0].aggregates[0] - expect(aggregate.owner).not.toBeUndefined() - expect(aggregate.owner?.ref).not.toBeUndefined() - expect(aggregate.owner?.ref?.name).toEqual('TestContext') + expect(aggregate.owner).toHaveLength(1) + expect(aggregate.owner[0].ref).not.toBeUndefined() + expect(aggregate.owner[0].ref?.name).toEqual('TestContext') }) test('check linking of shareholders context', async () => { diff --git a/test/parsing/ContextMappingModelParsing.test.ts b/test/parsing/ContextMappingModelParsing.test.ts index dad17a2..3fee4ae 100644 --- a/test/parsing/ContextMappingModelParsing.test.ts +++ b/test/parsing/ContextMappingModelParsing.test.ts @@ -19,7 +19,7 @@ describe('ContextMappingModel tests', () => { document = await parseValidInput(parse, '') expect(document.parseResult.value.valueRegisters).toHaveLength(0) - expect(document.parseResult.value.contextMaps).toHaveLength(0) + expect(document.parseResult.value.contextMap).toHaveLength(0) expect(document.parseResult.value.boundedContexts).toHaveLength(0) expect(document.parseResult.value.domains).toHaveLength(0) expect(document.parseResult.value.stakeholders).toHaveLength(0) diff --git a/test/parsing/boundedContext/AggregateParsing.test.ts b/test/parsing/boundedContext/AggregateParsing.test.ts index a9d0044..4d1c193 100644 --- a/test/parsing/boundedContext/AggregateParsing.test.ts +++ b/test/parsing/boundedContext/AggregateParsing.test.ts @@ -70,19 +70,19 @@ describe('Aggregate parsing tests', () => { expect(aggregate.responsibilities).toHaveLength(2) expect(aggregate.responsibilities[0]).toEqual('resp1') expect(aggregate.responsibilities[1]).toEqual('resp2') - expect(aggregate.owner).not.toBeUndefined() + expect(aggregate.owner).toHaveLength(1) expect(aggregate.userRequirements).toHaveLength(0) expect(aggregate.userStories).toHaveLength(0) expect(aggregate.useCases).toHaveLength(1) - expect(aggregate.knowledgeLevel).toEqual('META') - expect(aggregate.contentVolatility).toEqual('RARELY') - expect(aggregate.likelihoodForChange).toEqual('NORMAL') - expect(aggregate.availabilityCriticality).toEqual('HIGH') - expect(aggregate.consistencyCriticality).toEqual('HIGH') - expect(aggregate.securityZone).toEqual('testZone') - expect(aggregate.securityCriticality).toEqual('LOW') - expect(aggregate.securityAccessGroup).toEqual('testGroup') - expect(aggregate.storageSimilarity).toEqual('TINY') + expect(aggregate.knowledgeLevel).toEqual(['META']) + expect(aggregate.contentVolatility).toEqual(['RARELY']) + expect(aggregate.likelihoodForChange).toEqual(['NORMAL']) + expect(aggregate.availabilityCriticality).toEqual(['HIGH']) + expect(aggregate.consistencyCriticality).toEqual(['HIGH']) + expect(aggregate.securityZone).toEqual(['testZone']) + expect(aggregate.securityCriticality).toEqual(['LOW']) + expect(aggregate.securityAccessGroup).toEqual(['testGroup']) + expect(aggregate.storageSimilarity).toEqual(['TINY']) }) test('parse likelihood variation', async () => { @@ -96,7 +96,7 @@ describe('Aggregate parsing tests', () => { expect(document.parseResult.value.boundedContexts).toHaveLength(1) expect(document.parseResult.value.boundedContexts[0].aggregates).toHaveLength(1) - expect(document.parseResult.value.boundedContexts[0].aggregates[0].likelihoodForChange).toEqual('NORMAL') + expect(document.parseResult.value.boundedContexts[0].aggregates[0].likelihoodForChange).toEqual(['NORMAL']) }) test('parse userStory', async () => { @@ -160,14 +160,14 @@ function expectAggregateToBeEmpty (aggregate: Aggregate) { expect(aggregate.userRequirements).toHaveLength(0) expect(aggregate.useCases).toHaveLength(0) expect(aggregate.userStories).toHaveLength(0) - expect(aggregate.owner).toBeUndefined() - expect(aggregate.knowledgeLevel).toBeUndefined() - expect(aggregate.likelihoodForChange).toBeUndefined() - expect(aggregate.contentVolatility).toBeUndefined() - expect(aggregate.availabilityCriticality).toBeUndefined() - expect(aggregate.consistencyCriticality).toBeUndefined() - expect(aggregate.storageSimilarity).toBeUndefined() - expect(aggregate.securityCriticality).toBeUndefined() - expect(aggregate.securityZone).toBeUndefined() - expect(aggregate.securityAccessGroup).toBeUndefined() + expect(aggregate.owner).toHaveLength(0) + expect(aggregate.knowledgeLevel).toHaveLength(0) + expect(aggregate.likelihoodForChange).toHaveLength(0) + expect(aggregate.contentVolatility).toHaveLength(0) + expect(aggregate.availabilityCriticality).toHaveLength(0) + expect(aggregate.consistencyCriticality).toHaveLength(0) + expect(aggregate.storageSimilarity).toHaveLength(0) + expect(aggregate.securityCriticality).toHaveLength(0) + expect(aggregate.securityZone).toHaveLength(0) + expect(aggregate.securityAccessGroup).toHaveLength(0) } diff --git a/test/parsing/boundedContext/BoundedContextParsing.test.ts b/test/parsing/boundedContext/BoundedContextParsing.test.ts index 3c38145..7d91614 100644 --- a/test/parsing/boundedContext/BoundedContextParsing.test.ts +++ b/test/parsing/boundedContext/BoundedContextParsing.test.ts @@ -23,7 +23,7 @@ describe('BoundedContext parsing tests', () => { const contextMappingModel = document.parseResult.value expect(contextMappingModel).not.toBeUndefined() expect(contextMappingModel.boundedContexts.length).toEqual(1) - expect(contextMappingModel.contextMaps.length).toEqual(0) + expect(contextMappingModel.contextMap.length).toEqual(0) expect(contextMappingModel.userRequirements.length).toEqual(0) expect(contextMappingModel.domains.length).toEqual(0) expect(contextMappingModel.stakeholders.length).toEqual(0) @@ -33,15 +33,15 @@ describe('BoundedContext parsing tests', () => { expect(boundedContext).not.toBeUndefined() expect(boundedContext.name).toEqual('FirstContext') expect(boundedContext.realizedBoundedContexts.length).toEqual(0) - expect(boundedContext.refinedBoundedContext).toBeUndefined() + expect(boundedContext.refinedBoundedContext).toHaveLength(0) expect(boundedContext.implementedDomainParts.length).toEqual(0) - expect(boundedContext.domainVisionStatement).toBeUndefined() - expect(boundedContext.knowledgeLevel).toBeUndefined() - expect(boundedContext.type).toBeUndefined() + expect(boundedContext.domainVisionStatement).toHaveLength(0) + expect(boundedContext.knowledgeLevel).toHaveLength(0) + expect(boundedContext.type).toHaveLength(0) expect(boundedContext.responsibilities.length).toEqual(0) - expect(boundedContext.implementationTechnology).toBeUndefined() - expect(boundedContext.businessModel).toBeUndefined() - expect(boundedContext.evolution).toBeUndefined() + expect(boundedContext.implementationTechnology).toHaveLength(0) + expect(boundedContext.businessModel).toHaveLength(0) + expect(boundedContext.evolution).toHaveLength(0) expect(boundedContext.aggregates.length).toEqual(0) }) @@ -79,14 +79,14 @@ describe('BoundedContext parsing tests', () => { expect(boundedContext.name).toEqual('TestContext') expect(boundedContext.implementedDomainParts).toHaveLength(2) expect(boundedContext.realizedBoundedContexts).toHaveLength(1) - expect(boundedContext.refinedBoundedContext).not.toBeUndefined() - expect(boundedContext.domainVisionStatement).toEqual('vision') - expect(boundedContext.type).toEqual('UNDEFINED') - expect(boundedContext.implementationTechnology).toEqual('java') + expect(boundedContext.refinedBoundedContext).toHaveLength(1) + expect(boundedContext.domainVisionStatement).toEqual(['vision']) + expect(boundedContext.type).toEqual(['UNDEFINED']) + expect(boundedContext.implementationTechnology).toEqual(['java']) expect(boundedContext.responsibilities.length).toEqual(2) - expect(boundedContext.businessModel).toEqual('model') - expect(boundedContext.knowledgeLevel).toEqual('CONCRETE') - expect(boundedContext.evolution).toEqual('GENESIS') + expect(boundedContext.businessModel).toEqual(['model']) + expect(boundedContext.knowledgeLevel).toEqual(['CONCRETE']) + expect(boundedContext.evolution).toEqual(['GENESIS']) expect(boundedContext.aggregates.length).toEqual(0) }) @@ -106,13 +106,13 @@ describe('BoundedContext parsing tests', () => { const boundedContext = contextMappingModel.boundedContexts[0] expect(boundedContext).not.toBeUndefined() expect(boundedContext.name).toEqual('TestContext') - expect(boundedContext.domainVisionStatement).toEqual('vision') - expect(boundedContext.type).toEqual('FEATURE') - expect(boundedContext.implementationTechnology).toEqual('c#') + expect(boundedContext.domainVisionStatement).toEqual(['vision']) + expect(boundedContext.type).toEqual(['FEATURE']) + expect(boundedContext.implementationTechnology).toEqual(['c#']) expect(boundedContext.responsibilities.length).toEqual(0) - expect(boundedContext.businessModel).toBeUndefined() - expect(boundedContext.knowledgeLevel).toBeUndefined() - expect(boundedContext.evolution).toBeUndefined() + expect(boundedContext.businessModel).toHaveLength(0) + expect(boundedContext.knowledgeLevel).toHaveLength(0) + expect(boundedContext.evolution).toHaveLength(0) expect(boundedContext.aggregates.length).toEqual(0) }) }) diff --git a/test/parsing/boundedContext/SculptorModuleParsing.test.ts b/test/parsing/boundedContext/SculptorModuleParsing.test.ts index 549cbfe..a7524d6 100644 --- a/test/parsing/boundedContext/SculptorModuleParsing.test.ts +++ b/test/parsing/boundedContext/SculptorModuleParsing.test.ts @@ -60,9 +60,9 @@ describe('Sculptor module parsing tests', () => { const module = document.parseResult.value.boundedContexts[0].modules[0] expect(module.doc).toEqual('doc') expect(module.name).toEqual('TestModule') - expect(module.external).toEqual(true) - expect(module.basePackage).toEqual('base.package') - expect(module.hint).toEqual('hint') + expect(module.external).toEqual(['external']) + expect(module.basePackage).toEqual(['base.package']) + expect(module.hint).toEqual(['hint']) expect(module.aggregates).toHaveLength(1) }) }) @@ -71,8 +71,8 @@ function expectEmptyModule (module: SculptorModule): void { expect(module).not.toBeUndefined() expect(module.name).toEqual('TestModule') expect(module.doc).toBeUndefined() - expect(module.external).toEqual(false) - expect(module.basePackage).toBeUndefined() - expect(module.hint).toBeUndefined() + expect(module.external).toHaveLength(0) + expect(module.basePackage).toHaveLength(0) + expect(module.hint).toHaveLength(0) expect(module.aggregates).toHaveLength(0) } diff --git a/test/parsing/contextMap/ContextMapParsing.test.ts b/test/parsing/contextMap/ContextMapParsing.test.ts index 638b754..b3e39cb 100644 --- a/test/parsing/contextMap/ContextMapParsing.test.ts +++ b/test/parsing/contextMap/ContextMapParsing.test.ts @@ -21,13 +21,13 @@ describe('Context Map parsing tests', () => { } `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - const contextMap = document.parseResult.value.contextMaps[0] + expect(document.parseResult.value.contextMap).toHaveLength(1) + const contextMap = document.parseResult.value.contextMap[0] expect(contextMap).not.toBeUndefined() expect(contextMap.name).toBeUndefined() expect(contextMap.boundedContexts).toHaveLength(0) - expect(contextMap.type).toBeUndefined() - expect(contextMap.state).toBeUndefined() + expect(contextMap.type).toHaveLength(0) + expect(contextMap.state).toHaveLength(0) expect(contextMap.relationships).toHaveLength(0) }) @@ -37,8 +37,8 @@ describe('Context Map parsing tests', () => { } `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - const contextMap = document.parseResult.value.contextMaps[0] + expect(document.parseResult.value.contextMap).toHaveLength(1) + const contextMap = document.parseResult.value.contextMap[0] expect(contextMap).not.toBeUndefined() expect(contextMap.name).toEqual('TestMap') }) @@ -57,12 +57,12 @@ describe('Context Map parsing tests', () => { BoundedContext SecondContext `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - const contextMap = document.parseResult.value.contextMaps[0] + expect(document.parseResult.value.contextMap).toHaveLength(1) + const contextMap = document.parseResult.value.contextMap[0] expect(contextMap).not.toBeUndefined() expect(contextMap.name).toEqual('TestMap') - expect(contextMap.state).toEqual('AS_IS') - expect(contextMap.type).toEqual('ORGANIZATIONAL') + expect(contextMap.state).toEqual(['AS_IS']) + expect(contextMap.type).toEqual(['ORGANIZATIONAL']) expect(contextMap.boundedContexts).toHaveLength(2) expect(contextMap.relationships).toHaveLength(1) }) @@ -78,8 +78,8 @@ describe('Context Map parsing tests', () => { BoundedContext SecondContext `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - const contextMap = document.parseResult.value.contextMaps[0] + expect(document.parseResult.value.contextMap).toHaveLength(1) + const contextMap = document.parseResult.value.contextMap[0] expect(contextMap).not.toBeUndefined() expect(contextMap.boundedContexts).toHaveLength(2) }) diff --git a/test/parsing/contextMap/RelationshipParsing.test.ts b/test/parsing/contextMap/RelationshipParsing.test.ts index 78151ca..7abf5c7 100644 --- a/test/parsing/contextMap/RelationshipParsing.test.ts +++ b/test/parsing/contextMap/RelationshipParsing.test.ts @@ -31,9 +31,9 @@ describe('Relationship parsing tests', () => { BoundedContext TestContext `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - expect(document.parseResult.value.contextMaps[0].relationships).toHaveLength(1) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as SharedKernel + expect(document.parseResult.value.contextMap).toHaveLength(1) + expect(document.parseResult.value.contextMap[0].relationships).toHaveLength(1) + const relationship = document.parseResult.value.contextMap[0].relationships[0] as SharedKernel expect(relationship).not.toBeUndefined() expect(relationship.name).toEqual('RelName') expect(relationship.implementationTechnology).toEqual('Java') @@ -123,9 +123,9 @@ describe('Relationship parsing tests', () => { BoundedContext TestContext `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - expect(document.parseResult.value.contextMaps[0].relationships).toHaveLength(1) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as Partnership + expect(document.parseResult.value.contextMap).toHaveLength(1) + expect(document.parseResult.value.contextMap[0].relationships).toHaveLength(1) + const relationship = document.parseResult.value.contextMap[0].relationships[0] as Partnership expect(relationship).not.toBeUndefined() expect(relationship.name).toEqual('RelName') expect(relationship.implementationTechnology).toEqual('Java') @@ -208,13 +208,13 @@ describe('Relationship parsing tests', () => { } `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - expect(document.parseResult.value.contextMaps[0].relationships).toHaveLength(1) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as CustomerSupplierRelationship + expect(document.parseResult.value.contextMap).toHaveLength(1) + expect(document.parseResult.value.contextMap[0].relationships).toHaveLength(1) + const relationship = document.parseResult.value.contextMap[0].relationships[0] as CustomerSupplierRelationship expect(relationship).not.toBeUndefined() expect(relationship.name).toEqual('RelName') - expect(relationship.implementationTechnology).toEqual('Java') - expect(relationship.downstreamGovernanceRights).toEqual('INFLUENCER') + expect(relationship.implementationTechnology).toEqual(['Java']) + expect(relationship.downstreamGovernanceRights).toEqual(['INFLUENCER']) expect(relationship.upstreamExposedAggregates).toHaveLength(1) expect(relationship.upstream).not.toBeUndefined() expect(relationship.downstream).not.toBeUndefined() @@ -285,14 +285,14 @@ describe('Relationship parsing tests', () => { BoundedContext TestContext `) - expect(document.parseResult.value.contextMaps).toHaveLength(1) - expect(document.parseResult.value.contextMaps[0].relationships).toHaveLength(1) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] as UpstreamDownstreamRelationship + expect(document.parseResult.value.contextMap).toHaveLength(1) + expect(document.parseResult.value.contextMap[0].relationships).toHaveLength(1) + const relationship = document.parseResult.value.contextMap[0].relationships[0] as UpstreamDownstreamRelationship expect(relationship).not.toBeUndefined() expect(relationship.name).toEqual('RelName') - expect(relationship.downstreamGovernanceRights).toEqual('INFLUENCER') + expect(relationship.downstreamGovernanceRights).toEqual(['INFLUENCER']) expect(relationship.upstreamExposedAggregates).toHaveLength(1) - expect(relationship.implementationTechnology).toEqual('Java') + expect(relationship.implementationTechnology).toEqual(['Java']) expect(relationship.upstream).not.toBeUndefined() expect(relationship.upstreamRoles).toHaveLength(1) expect(relationship.upstreamRoles[0]).toEqual('OHS') @@ -351,9 +351,9 @@ describe('Relationship parsing tests', () => { }) function expectRelationshipType (document: LangiumDocument, type: string) { - expect(document.parseResult.value.contextMaps).toHaveLength(1) - expect(document.parseResult.value.contextMaps[0].relationships).toHaveLength(1) - const relationship = document.parseResult.value.contextMaps[0].relationships[0] + expect(document.parseResult.value.contextMap).toHaveLength(1) + expect(document.parseResult.value.contextMap[0].relationships).toHaveLength(1) + const relationship = document.parseResult.value.contextMap[0].relationships[0] expect(relationship).not.toBeUndefined() expect(relationship.$type).toEqual(type) } diff --git a/test/parsing/domain/DomainParsing.test.ts b/test/parsing/domain/DomainParsing.test.ts index e267618..c80ff54 100644 --- a/test/parsing/domain/DomainParsing.test.ts +++ b/test/parsing/domain/DomainParsing.test.ts @@ -78,7 +78,7 @@ describe('Domain parsing tests', () => { expect(subdomain).not.toBeUndefined() expect(subdomain.name).toEqual('TestSubdomain') expect(subdomain.supportedFeatures).toHaveLength(1) - expect(subdomain.type).toEqual('CORE_DOMAIN') - expect(subdomain.domainVisionStatement).toEqual('vision') + expect(subdomain.type).toEqual(['CORE_DOMAIN']) + expect(subdomain.domainVisionStatement).toEqual(['vision']) }) }) diff --git a/test/parsing/requirements/UserRequirementParsing.test.ts b/test/parsing/requirements/UserRequirementParsing.test.ts index f2e54b5..9b9320d 100644 --- a/test/parsing/requirements/UserRequirementParsing.test.ts +++ b/test/parsing/requirements/UserRequirementParsing.test.ts @@ -60,10 +60,10 @@ describe('User requirement parsing tests', () => { expect(useCase.secondaryActors).toHaveLength(2) expect(useCase.secondaryActors[0]).toEqual('actor1') expect(useCase.secondaryActors[1]).toEqual('actor2') - expect(useCase.role).toEqual('role') - expect(useCase.benefit).toEqual('benefit') - expect(useCase.level).toEqual('level') - expect(useCase.scope).toEqual('scope') + expect(useCase.role).toEqual(['role']) + expect(useCase.benefit).toEqual(['benefit']) + expect(useCase.level).toEqual(['level']) + expect(useCase.scope).toEqual(['scope']) expect(useCase.features).toHaveLength(2) }) @@ -166,12 +166,12 @@ describe('User requirement parsing tests', () => { function expectUseCaseToBeEmpty (useCase: UseCase) { expect(useCase.name).toEqual('TestUseCase') - expect(useCase.role).toBeUndefined() + expect(useCase.role).toHaveLength(0) expect(useCase.secondaryActors).toHaveLength(0) expect(useCase.features).toHaveLength(0) - expect(useCase.benefit).toBeUndefined() - expect(useCase.scope).toBeUndefined() - expect(useCase.level).toBeUndefined() + expect(useCase.benefit).toHaveLength(0) + expect(useCase.scope).toHaveLength(0) + expect(useCase.level).toHaveLength(0) } function expectUserStoryToBeEmpty (userStory: UserStory) { diff --git a/test/parsing/vdad/StakeholdersParsing.test.ts b/test/parsing/vdad/StakeholdersParsing.test.ts index c701dcf..d319c2b 100644 --- a/test/parsing/vdad/StakeholdersParsing.test.ts +++ b/test/parsing/vdad/StakeholdersParsing.test.ts @@ -139,9 +139,9 @@ describe('Stakeholders parsing tests', () => { expect(document.parseResult.value.stakeholders[0].stakeholders).toHaveLength(1) const stakeholder = document.parseResult.value.stakeholders[0].stakeholders[0] as Stakeholder expect(stakeholder.name).toEqual('TestStakeholder') - expect(stakeholder.interest).toEqual('HIGH') - expect(stakeholder.influence).toEqual('MEDIUM') - expect(stakeholder.description).toEqual('description') + expect(stakeholder.interest).toEqual(['HIGH']) + expect(stakeholder.influence).toEqual(['MEDIUM']) + expect(stakeholder.description).toEqual(['description']) }) }) @@ -161,7 +161,7 @@ function expectEmptyStakeholderGroup (group: StakeholderGroup): void { function expectEmptyStakeholder (stakeholder: Stakeholder): void { expect(stakeholder).not.toBeUndefined() expect(stakeholder.name).toEqual('TestStakeholder') - expect(stakeholder.influence).toBeUndefined() - expect(stakeholder.interest).toBeUndefined() - expect(stakeholder.description).toBeUndefined() + expect(stakeholder.influence).toHaveLength(0) + expect(stakeholder.interest).toHaveLength(0) + expect(stakeholder.description).toHaveLength(0) } diff --git a/test/parsing/vdad/ValueRegisterParsing.test.ts b/test/parsing/vdad/ValueRegisterParsing.test.ts index 89269f7..102880c 100644 --- a/test/parsing/vdad/ValueRegisterParsing.test.ts +++ b/test/parsing/vdad/ValueRegisterParsing.test.ts @@ -377,8 +377,8 @@ describe('Value register parsing tests', () => { expect(document.parseResult.value.valueRegisters[0].values[0].elicitations).toHaveLength(1) const elicitation = document.parseResult.value.valueRegisters[0].values[0].elicitations[0] expect(elicitation.stakeholder).not.toBeUndefined() - expect(elicitation.priority).toEqual('LOW') - expect(elicitation.impact).toEqual('MEDIUM') + expect(elicitation.priority).toEqual(['LOW']) + expect(elicitation.impact).toEqual(['MEDIUM']) expect(elicitation.consequences).toHaveLength(1) }) @@ -465,7 +465,7 @@ function expectEmptyEpic (epic: ValueEpic) { function expectEmptyValueElicitation (elicitation: ValueElicitation) { expect(elicitation).not.toBeUndefined() expect(elicitation.stakeholder).not.toBeUndefined() - expect(elicitation.priority).toBeUndefined() - expect(elicitation.impact).toBeUndefined() + expect(elicitation.priority).toHaveLength(0) + expect(elicitation.impact).toHaveLength(0) expect(elicitation.consequences).toHaveLength(0) } diff --git a/test/validation/AggregateValidator.test.ts b/test/validation/AggregateValidator.test.ts new file mode 100644 index 0000000..d649c6b --- /dev/null +++ b/test/validation/AggregateValidator.test.ts @@ -0,0 +1,288 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('AggregateValidationProvider tests', () => { + test('accept one owner', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + owner TestOwner + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple owners', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + owner TestOwner + owner TestOwner + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one knowledgeLevel', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + knowledgeLevel META + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple knowledgeLevels', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + knowledgeLevel META + knowledgeLevel META + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one contentVolatility', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + contentVolatility NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple contentVolatilities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + contentVolatility NORMAL + contentVolatility NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one availabilityCriticality', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + availabilityCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple availabilityCriticalities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + availabilityCriticality NORMAL + availabilityCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one consistencyCriticality', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + consistencyCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple consistencyCriticalities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + consistencyCriticality NORMAL + consistencyCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one storageSimilarity', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + storageSimilarity NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple storageSimilarities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + storageSimilarity NORMAL + storageSimilarity NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one securityCriticality', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple securityCriticalities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityCriticality NORMAL + securityCriticality NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one securityZone', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityZone "zone" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple securityZones', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityZone "zone" + securityZone "zone" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one securityAccessGroup', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityAccessGroup "group" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple securityAccessGroups', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + securityAccessGroup "group" + securityAccessGroup "group" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one likelihoodForChange', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + likelihoodForChange NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('accept one structuralVolatility', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + structuralVolatility NORMAL + } + } + `) + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple structuralVolatilities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + structuralVolatility NORMAL + likelihoodForChange NORMAL + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) +}) diff --git a/test/validation/BoundedContextValidator.test.ts b/test/validation/BoundedContextValidator.test.ts new file mode 100644 index 0000000..f7c5885 --- /dev/null +++ b/test/validation/BoundedContextValidator.test.ts @@ -0,0 +1,179 @@ +import { beforeAll, describe, expect, test } from 'vitest' +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { EmptyFileSystem, type LangiumDocument } from 'langium' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('BoundedContextValidationProvider tests', () => { + // TODO: test implementedDomainParts & realizedBoundedContexts after regex enforcement impl + + test('accept one refinement', async () => { + document = await parse(` + BoundedContext FirstContext + refines OtherContext { + + } + BoundedContext OtherContext + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple refinements', async () => { + document = await parse(` + BoundedContext FirstContext + refines OtherContext + refines ThirdContext { + } + BoundedContext OtherContext + BoundedContext ThirdContext + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one domainVisionStatement', async () => { + document = await parse(` + BoundedContext FirstContext { + domainVisionStatement "This is a domain vision statement" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple domainVisionStatements', async () => { + document = await parse(` + BoundedContext FirstContext { + domainVisionStatement "This is a domain vision statement" + domainVisionStatement "This is another domain vision statement" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one type', async () => { + document = await parse(` + BoundedContext FirstContext { + type UNDEFINED + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple types', async () => { + document = await parse(` + BoundedContext FirstContext { + type UNDEFINED + type TEAM + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + // TODO: test responsibilities after Regex validation impl + + test('accept one implementationTechnology', async () => { + document = await parse(` + BoundedContext FirstContext { + implementationTechnology "This is an implementation technology" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple implementationTechnologies', async () => { + document = await parse(` + BoundedContext FirstContext { + implementationTechnology "This is an implementation technology" + implementationTechnology "This is another implementation technology" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one knowledgeLevel', async () => { + document = await parse(` + BoundedContext FirstContext { + knowledgeLevel META + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple knowledgeLevels', async () => { + document = await parse(` + BoundedContext FirstContext { + knowledgeLevel META + knowledgeLevel CONCRETE + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one businessModel', async () => { + document = await parse(` + BoundedContext FirstContext { + businessModel "This is a business model" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple businessModels', async () => { + document = await parse(` + BoundedContext FirstContext { + businessModel "This is a business model" + businessModel "This is another business model" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one evolution', async () => { + document = await parse(` + BoundedContext FirstContext { + evolution UNDEFINED + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple evolutions', async () => { + document = await parse(` + BoundedContext FirstContext { + evolution UNDEFINED + evolution UNDEFINED + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) +}) diff --git a/test/validation/ContextMapValidator.test.ts b/test/validation/ContextMapValidator.test.ts new file mode 100644 index 0000000..35d7d46 --- /dev/null +++ b/test/validation/ContextMapValidator.test.ts @@ -0,0 +1,74 @@ +import { beforeAll, describe, expect, test } from 'vitest' +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { EmptyFileSystem, type LangiumDocument } from 'langium' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('ContextMapValidationProvider tests', () => { + test('accept no attribute', async () => { + document = await parse(` + ContextMap { + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('accept one type attribute', async () => { + document = await parse(` + ContextMap { + type UNDEFINED + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('accept one state attribute', async () => { + document = await parse(` + ContextMap { + state AS_IS + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple type attributes', async () => { + document = await parse(` + ContextMap { + type UNDEFINED + type SYSTEM_LANDSCAPE + } + `) + + expect(document.diagnostics).not.toBeUndefined() + expect(document.diagnostics).toHaveLength(1) + const diagnostic = document.diagnostics![0] + expect(diagnostic.range.start.line).toEqual(2) + }) + + test('report multiple state attributes', async () => { + document = await parse(` + ContextMap { + state AS_IS + state TO_BE + } + `) + + expect(document.diagnostics).not.toBeUndefined() + expect(document.diagnostics).toHaveLength(1) + const diagnostic = document.diagnostics![0] + expect(diagnostic.range.start.line).toEqual(2) + }) +}) diff --git a/test/validation/ContextMappingModelValidator.test.ts b/test/validation/ContextMappingModelValidator.test.ts index fb2d66d..8551d7c 100644 --- a/test/validation/ContextMappingModelValidator.test.ts +++ b/test/validation/ContextMappingModelValidator.test.ts @@ -12,9 +12,6 @@ beforeAll(async () => { services = createContextMapperDslServices(EmptyFileSystem) const doParse = parseHelper(services.ContextMapperDsl) parse = (input: string) => doParse(input, { validation: true }) - - // activate the following if your linking test requires elements from a built-in library, for example - // await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); }) describe('ContextMappingModelValidationProvider tests', () => { diff --git a/test/validation/SculptorModuleValidator.test.ts b/test/validation/SculptorModuleValidator.test.ts new file mode 100644 index 0000000..564d9db --- /dev/null +++ b/test/validation/SculptorModuleValidator.test.ts @@ -0,0 +1,95 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('SculptorModuleValidationProvider tests', () => { + test('accept one external attribute', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + external + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple external attributes', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + external + external + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one basePackage', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + basePackage = test.package + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple basePackages', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + basePackage = test.package + basePackage = test.package + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one hint', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + hint = "hint" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple hints', async () => { + document = await parse(` + BoundedContext TextContext { + Module TestModule { + hint = "hint" + hint = "hint" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) +}) diff --git a/test/validation/StakeholderValidator.test.ts b/test/validation/StakeholderValidator.test.ts new file mode 100644 index 0000000..0806326 --- /dev/null +++ b/test/validation/StakeholderValidator.test.ts @@ -0,0 +1,95 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('StakeholderValidationProvider tests', () => { + test('accept one influence', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + influence HIGH + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple influences', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + influence HIGH + influence HIGH + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one interest', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + interest HIGH + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple interests', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + interest HIGH + interest HIGH + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one description', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + description "Test Description" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple descriptions', async () => { + document = await parse(` + Stakeholders { + Stakeholder TestStakeholder { + description "Test Description" + description "Test Description" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) +}) diff --git a/test/validation/SubDomainValidator.test.ts b/test/validation/SubDomainValidator.test.ts new file mode 100644 index 0000000..40000bc --- /dev/null +++ b/test/validation/SubDomainValidator.test.ts @@ -0,0 +1,69 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('SubDomainValidationProvider tests', () => { + test('accept one type', async () => { + document = await parse(` + Domain TestDomain { + Subdomain TestSubdomain { + type UNDEFINED + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple types', async () => { + document = await parse(` + Domain TestDomain { + Subdomain TestSubdomain { + type UNDEFINED + type CORE_DOMAIN + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one domainVisionStatement', async () => { + document = await parse(` + Domain TestDomain { + Subdomain TestSubdomain { + domainVisionStatement "Test Vision Statement" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple domainVisionStatements', async () => { + document = await parse(` + Domain TestDomain { + Subdomain TestSubdomain { + domainVisionStatement "Test Vision Statement" + domainVisionStatement "Test Vision Statement" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) +}) diff --git a/test/validation/UseCaseValidator.test.ts b/test/validation/UseCaseValidator.test.ts new file mode 100644 index 0000000..69c591a --- /dev/null +++ b/test/validation/UseCaseValidator.test.ts @@ -0,0 +1,105 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('UseCaseValidationProvider tests', () => { + test('accept one actor', async () => { + document = await parse(` + UseCase TestUseCase { + actor "Test Actor" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple actors', async () => { + document = await parse(` + UseCase TestUseCase { + actor "Test Actor" + actor "Test Actor" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one benefit', async () => { + document = await parse(` + UseCase TestUseCase { + benefit "Test Benefit" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple benefits', async () => { + document = await parse(` + UseCase TestUseCase { + benefit "Test Benefit" + benefit "Test Benefit" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one scope', async () => { + document = await parse(` + UseCase TestUseCase { + scope "Test Scope" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple scopes', async () => { + document = await parse(` + UseCase TestUseCase { + scope "Test Scope" + scope "Test Scope" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one level', async () => { + document = await parse(` + UseCase TestUseCase { + level "Test Level" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple levels', async () => { + document = await parse(` + UseCase TestUseCase { + level "Test Level" + level "Test Level" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) +}) diff --git a/test/validation/ValueElicitationValidator.test.ts b/test/validation/ValueElicitationValidator.test.ts new file mode 100644 index 0000000..1ee6928 --- /dev/null +++ b/test/validation/ValueElicitationValidator.test.ts @@ -0,0 +1,108 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('ValueElicitationValidationProvider tests', () => { + test('accept one priority', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + priority = HIGH + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple priorities', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + priority = HIGH + priority = HIGH + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(4) + }) + + test('accept one impact', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + impact HIGH + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple impacts', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + impact HIGH + impact HIGH + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(4) + }) + + test('report multiple impacts', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + impact HIGH + impact HIGH + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(4) + }) +}) diff --git a/test/validation/ValueValidator.test.ts b/test/validation/ValueValidator.test.ts index ca915cd..db65fce 100644 --- a/test/validation/ValueValidator.test.ts +++ b/test/validation/ValueValidator.test.ts @@ -12,9 +12,6 @@ beforeAll(async () => { services = createContextMapperDslServices(EmptyFileSystem) const doParse = parseHelper(services.ContextMapperDsl) parse = (input: string) => doParse(input, { validation: true }) - - // activate the following if your linking test requires elements from a built-in library, for example - // await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); }) describe('ContextMappingModelValidationProvider tests', () => { From 7ed7e8947c6d075e3ca994664bb3b1434b12b248 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Thu, 1 May 2025 11:24:25 +0200 Subject: [PATCH 4/9] cleanup --- src/language/ContextMapperDslModule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/language/ContextMapperDslModule.ts b/src/language/ContextMapperDslModule.ts index e5559bb..c763e94 100644 --- a/src/language/ContextMapperDslModule.ts +++ b/src/language/ContextMapperDslModule.ts @@ -16,7 +16,6 @@ import { ContextMapperValidationProviderRegistry } from './validation/ContextMap import { ContextMapperDslScopeProvider } from './references/ContextMapperDslScopeProvider.js' import { ContextMapperDslFoldingRangeProvider } from './folding/ContextMapperDslFoldingRageProvider.js' import { ContextMapperDslScopeComputation } from './references/ContextMapperDslScopeComputation.js' -import { ContextMapperDslCompletionProvider } from './completion/ContextMapperDslCompletionProvider.js' /** * Declaration of custom services - add your own service classes here. From d1bf996a3367cf8acf365b9ffa568c114d34754a Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Thu, 1 May 2025 11:24:38 +0200 Subject: [PATCH 5/9] add some autocomplete tests --- test/completion/Completion.test.ts | 62 ++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/test/completion/Completion.test.ts b/test/completion/Completion.test.ts index ce89dec..d507c5b 100644 --- a/test/completion/Completion.test.ts +++ b/test/completion/Completion.test.ts @@ -5,11 +5,11 @@ import { ContextMappingModel } from '../../src/language/generated/ast.js' import { EmptyFileSystem, LangiumDocument } from 'langium' import { afterEach, beforeAll, describe, expect, test } from 'vitest' import { fail } from 'node:assert' +import { uinteger } from 'vscode-languageserver-types' let services: ReturnType let completionProvider: CompletionProvider let parse: ReturnType> -let document: LangiumDocument | undefined beforeAll(async () => { services = createContextMapperDslServices(EmptyFileSystem) @@ -18,7 +18,7 @@ beforeAll(async () => { }) afterEach(async () => { - document && await clearDocuments(services.shared, [document]) + await clearDocuments(services.shared, services.shared.workspace.LangiumDocuments.all.toArray()) }) describe('Completion tests', () => { @@ -40,20 +40,60 @@ describe('Completion tests', () => { BoundedContext AnotherContext `) - const params = { - textDocument: { - uri: docToComplete.uri.path - }, - position: { - line: 2, - character: 28 - } - } + const params = createCompletionParams(docToComplete, 2, 28) const completionList = await completionProvider.getCompletion(docToComplete, params) if (completionList == null) { fail('Expected completion provider to return completion list') + return } expect(completionList.items).toHaveLength(1) expect(completionList.items[0].label).toEqual('TestContext') }) + + test('check completion of bounded context property', async () => { + const documentToComplete = await parse(` + BoundedContext TestContext { + typ + } + `) + + const params = createCompletionParams(documentToComplete, 2, 11) + const completionList = await completionProvider.getCompletion(documentToComplete, params) + if (completionList == null) { + fail('Expected completion provider to return completion list') + return + } + expect(completionList.items).toHaveLength(1) + expect(completionList.items[0].label).toEqual('type') + }) + + test('check completion of bounded context property with existing property', async () => { + const documentToComplete = await parse(` + ContextMap { + state UNDEFINED + ty + } + `) + + const params = createCompletionParams(documentToComplete, 3, 10) + const completionList = await completionProvider.getCompletion(documentToComplete, params) + if (completionList == null) { + fail('Expected completion provider to return completion list') + return + } + expect(completionList.items).toHaveLength(1) + expect(completionList.items[0].label).toEqual('type') + }) }) + +function createCompletionParams (document: LangiumDocument, positionLine: uinteger, positionChar: uinteger): any { + return { + textDocument: { + uri: document.uri.path + }, + position: { + line: positionLine, + character: positionChar + } + } +} From 889a1f29491581d3aa9c6fb39a4a591765303cd6 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Fri, 2 May 2025 11:33:36 +0200 Subject: [PATCH 6/9] add validations & tests for non-repetition of list attributes --- src/language/context-mapper-dsl.langium | 8 +- ...ContextMapperValidationProviderRegistry.ts | 6 +- src/language/validation/ValidationHelper.ts | 55 +++++- .../impl/AggregateValidationProvider.ts | 30 ++- .../impl/BoundedContextValidationProvider.ts | 10 +- ...ownstreamRelationshipValidationProvider.ts | 6 +- .../impl/UseCaseValidationProvider.ts | 8 +- .../ValueElicitationValidationProvider.ts | 4 +- .../impl/ValueEpicValidationProvider.ts | 15 +- .../impl/ValueValidationProvider.ts | 2 +- test/validation/AggregateValidator.test.ts | 185 ++++++++++++++++++ .../BoundedContextValidator.test.ts | 82 +++++++- ...eamDownstreamRelationshipValidator.test.ts | 116 +++++++++++ test/validation/UseCaseValidator.test.ts | 49 +++++ test/validation/ValidationHelper.test.ts | 118 +++++++++++ .../ValueElicitationValidator.test.ts | 41 ++++ test/validation/ValueEpicValidator.test.ts | 83 ++++++++ 17 files changed, 770 insertions(+), 48 deletions(-) create mode 100644 test/validation/UpstreamDownstreamRelationshipValidator.test.ts create mode 100644 test/validation/ValidationHelper.test.ts create mode 100644 test/validation/ValueEpicValidator.test.ts diff --git a/src/language/context-mapper-dsl.langium b/src/language/context-mapper-dsl.langium index c0d6e34..de90da0 100644 --- a/src/language/context-mapper-dsl.langium +++ b/src/language/context-mapper-dsl.langium @@ -177,11 +177,9 @@ Aggregate: "Aggregate" name=ID (OPEN ( (('responsibilities' ('=')? responsibilities+=STRING) ("," responsibilities+=STRING)*) | - ( - (('useCases' ('=')? useCases+=[UseCase]) ("," useCases+=[UseCase])*) | - (('userStories' ('=')? userStories+=[UserStory]) ("," userStories+=[UserStory])*) | - ((('features' | 'userRequirements') ('=')? userRequirements+=[UserRequirement]) ("," userRequirements+=[UserRequirement])*) - ) | + (('useCases' ('=')? useCases+=[UseCase]) ("," useCases+=[UseCase])*) | + (('userStories' ('=')? userStories+=[UserStory]) ("," userStories+=[UserStory])*) | + ((('features' | 'userRequirements') ('=')? userRequirements+=[UserRequirement]) ("," userRequirements+=[UserRequirement])*) | ('owner' ('=')? owner+=[BoundedContext]) | ('knowledgeLevel' ('=')? knowledgeLevel+=KnowledgeLevel) | (('likelihoodForChange' | 'structuralVolatility') ('=')? likelihoodForChange+=Volatility) | diff --git a/src/language/validation/ContextMapperValidationProviderRegistry.ts b/src/language/validation/ContextMapperValidationProviderRegistry.ts index 576550f..a7f81d5 100644 --- a/src/language/validation/ContextMapperValidationProviderRegistry.ts +++ b/src/language/validation/ContextMapperValidationProviderRegistry.ts @@ -14,7 +14,7 @@ import { UpstreamDownstreamRelationship, UseCase, Value, - ValueElicitation + ValueElicitation, ValueEpic } from '../generated/ast.js' import { BoundedContextValidationProvider } from './impl/BoundedContextValidationProvider.js' import { SubDomainValidationProvider } from './impl/SubDomainValidationProvider.js' @@ -26,6 +26,7 @@ import { UseCaseValidationProvider } from './impl/UseCaseValidationProvider.js' import { SculptorModuleValidationProvider } from './impl/SculptorModuleValidationProvider.js' import { StakeholderValidationProvider } from './impl/StakeholderValidationProvider.js' import { ValueElicitationValidationProvider } from './impl/ValueElicitationValidationProvider.js' +import { ValueEpicValidationProvider } from './impl/ValueEpicValidationProvider.js' export class ContextMapperValidationProviderRegistry { private readonly _providers = new Map>([ @@ -39,7 +40,8 @@ export class ContextMapperValidationProviderRegistry { [UseCase, new UseCaseValidationProvider()], [SculptorModule, new SculptorModuleValidationProvider()], [Stakeholder, new StakeholderValidationProvider()], - [ValueElicitation, new ValueElicitationValidationProvider()] + [ValueElicitation, new ValueElicitationValidationProvider()], + [ValueEpic, new ValueEpicValidationProvider()] ]) get (node: AstNode): ContextMapperValidationProvider | undefined { diff --git a/src/language/validation/ValidationHelper.ts b/src/language/validation/ValidationHelper.ts index ba85dab..2fdd8ec 100644 --- a/src/language/validation/ValidationHelper.ts +++ b/src/language/validation/ValidationHelper.ts @@ -1,18 +1,57 @@ import { AstNode, ValidationAcceptor } from 'langium' -export function enforceZeroOrOneCardinality (node: AstNode, property: string, acceptor: ValidationAcceptor, propertyName: string = property) { +export function enforceZeroOrOneCardinality (node: AstNode, property: string, acceptor: ValidationAcceptor, keywords: string[] = [property]) { const nodeProperty = node[property as keyof AstNode] - if (!Array.isArray(nodeProperty)) { - acceptor('warning', `There was a problem validating the element ${propertyName}.`, { - node, - property + if (nodeProperty != null && !Array.isArray(nodeProperty)) { + keywords.forEach(keyword => { + acceptor('warning', `There was a problem validating the attribute ${keyword}.`, { + node, + keyword + }) }) return } if (nodeProperty != null && nodeProperty.length > 1) { - acceptor('error', `There must be zero or one ${propertyName} attribute`, { - node, - property + keywords.forEach(keyword => { + acceptor('error', `There must be zero or one ${keyword} attribute`, { + node, + keyword + }) + }) + } +} + +export function enforceZeroOrOneCardinalityOfListAttribute (node: AstNode, property: string, acceptor: ValidationAcceptor, keywords: string[] = [property]) { + const nodeProperty = node[property as keyof AstNode] + if (!Array.isArray(nodeProperty)) { + keywords.forEach(keyword => { + acceptor('warning', `There was a problem validating the attribute "${keywords.join(' | ')}".`, { + node, + keyword + }) + }) + return + } + + if (nodeProperty == null || nodeProperty.length < 2 || node.$cstNode == null) { + return + } + + let matchCount = 0 + keywords.forEach(keyword => { + const regex = new RegExp(`(? 1) { + keywords.forEach(keyword => { + acceptor('error', `There must be zero or one "${keywords.join(' | ')}" attribute`, { + node, + keyword + }) }) } } diff --git a/src/language/validation/impl/AggregateValidationProvider.ts b/src/language/validation/impl/AggregateValidationProvider.ts index 84f7ee5..a199c36 100644 --- a/src/language/validation/impl/AggregateValidationProvider.ts +++ b/src/language/validation/impl/AggregateValidationProvider.ts @@ -1,18 +1,17 @@ import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' import { Aggregate } from '../../generated/ast.js' -import { ValidationAcceptor } from 'langium' -import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' +import { Properties, ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' export class AggregateValidationProvider implements ContextMapperValidationProvider { validate (node: Aggregate, acceptor: ValidationAcceptor): void { - // TODO: regex enforce responsibilities - // TODO: regex enforce useCases - // TODO: regex enforce userStories - // TODO: regex enforce userRequirements & features - + enforceZeroOrOneCardinalityOfListAttribute(node, 'responsibilities', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'useCases', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'userStories', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'userRequirements', acceptor, ['userRequirements', 'features']) enforceZeroOrOneCardinality(node, 'owner', acceptor) enforceZeroOrOneCardinality(node, 'knowledgeLevel', acceptor) - enforceZeroOrOneCardinality(node, 'likelihoodForChange', acceptor) + enforceZeroOrOneCardinality(node, 'likelihoodForChange', acceptor, ['likelihoodForChange', 'structuralVolatility']) enforceZeroOrOneCardinality(node, 'contentVolatility', acceptor) enforceZeroOrOneCardinality(node, 'availabilityCriticality', acceptor) enforceZeroOrOneCardinality(node, 'consistencyCriticality', acceptor) @@ -20,5 +19,20 @@ export class AggregateValidationProvider implements ContextMapperValidationProvi enforceZeroOrOneCardinality(node, 'securityCriticality', acceptor) enforceZeroOrOneCardinality(node, 'securityZone', acceptor) enforceZeroOrOneCardinality(node, 'securityAccessGroup', acceptor) + + // make sure only one of the userRequirements keywords is used + const userRequirementProperties = ['userRequirements', 'useCases', 'userStories'] as Array + const setUserRequirements = userRequirementProperties.filter(p => { + const value = node[p] as Array + return value.length > 0 + }) + if (setUserRequirements.length > 1) { + setUserRequirements.forEach(property => { + acceptor('error', 'One ony of the keywords "userRequirements", "features", "useCases" and "userStories" may be used', { + node, + property: property as Properties + }) + }) + } } } diff --git a/src/language/validation/impl/BoundedContextValidationProvider.ts b/src/language/validation/impl/BoundedContextValidationProvider.ts index 4fa1151..068f184 100644 --- a/src/language/validation/impl/BoundedContextValidationProvider.ts +++ b/src/language/validation/impl/BoundedContextValidationProvider.ts @@ -1,17 +1,17 @@ import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' import { BoundedContext } from '../../generated/ast.js' import { ValidationAcceptor } from 'langium' -import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' export class BoundedContextValidationProvider implements ContextMapperValidationProvider { validate (node: BoundedContext, acceptor: ValidationAcceptor): void { - // TODO: regex enforce implementedDomainParts - // TODO: regex enforce realizedBoundedContexts - enforceZeroOrOneCardinality(node, 'refinedBoundedContext', acceptor, 'refines') + enforceZeroOrOneCardinalityOfListAttribute(node, 'implementedDomainParts', acceptor, ['implements']) + enforceZeroOrOneCardinalityOfListAttribute(node, 'realizedBoundedContexts', acceptor, ['realizes']) + enforceZeroOrOneCardinality(node, 'refinedBoundedContext', acceptor, ['refines']) enforceZeroOrOneCardinality(node, 'domainVisionStatement', acceptor) enforceZeroOrOneCardinality(node, 'type', acceptor) - // TODO: regex enforce responsibilities + enforceZeroOrOneCardinalityOfListAttribute(node, 'responsibilities', acceptor) enforceZeroOrOneCardinality(node, 'implementationTechnology', acceptor) enforceZeroOrOneCardinality(node, 'knowledgeLevel', acceptor) enforceZeroOrOneCardinality(node, 'businessModel', acceptor) diff --git a/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts b/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts index a3293f5..e6a4412 100644 --- a/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts +++ b/src/language/validation/impl/UpstreamDownstreamRelationshipValidationProvider.ts @@ -1,12 +1,12 @@ import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' import { UpstreamDownstreamRelationship } from '../../generated/ast.js' import { ValidationAcceptor } from 'langium' -import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' export class UpstreamDownstreamRelationshipValidationProvider implements ContextMapperValidationProvider { validate (node: UpstreamDownstreamRelationship, acceptor: ValidationAcceptor): void { enforceZeroOrOneCardinality(node, 'implementationTechnology', acceptor) - // TODO: regex enforce exposedAggregates - enforceZeroOrOneCardinality(node, 'downstreamRights', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'upstreamExposedAggregates', acceptor, ['exposedAggregates']) + enforceZeroOrOneCardinality(node, 'downstreamGovernanceRights', acceptor, ['downstreamRights']) } } diff --git a/src/language/validation/impl/UseCaseValidationProvider.ts b/src/language/validation/impl/UseCaseValidationProvider.ts index 9fe35eb..7480e42 100644 --- a/src/language/validation/impl/UseCaseValidationProvider.ts +++ b/src/language/validation/impl/UseCaseValidationProvider.ts @@ -1,14 +1,14 @@ import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' import { isUseCase, UserRequirement } from '../../generated/ast.js' import { ValidationAcceptor } from 'langium' -import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' export class UseCaseValidationProvider implements ContextMapperValidationProvider { validate (node: UserRequirement, acceptor: ValidationAcceptor): void { if (isUseCase(node)) { - enforceZeroOrOneCardinality(node, 'role', acceptor, 'actor') - // TODO: regex enforce secondaryActors - // TODO: regex enforce features + enforceZeroOrOneCardinality(node, 'role', acceptor, ['actor']) + enforceZeroOrOneCardinalityOfListAttribute(node, 'secondaryActors', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'features', acceptor, ['interactions']) enforceZeroOrOneCardinality(node, 'benefit', acceptor) enforceZeroOrOneCardinality(node, 'scope', acceptor) enforceZeroOrOneCardinality(node, 'level', acceptor) diff --git a/src/language/validation/impl/ValueElicitationValidationProvider.ts b/src/language/validation/impl/ValueElicitationValidationProvider.ts index 8b60223..88301c9 100644 --- a/src/language/validation/impl/ValueElicitationValidationProvider.ts +++ b/src/language/validation/impl/ValueElicitationValidationProvider.ts @@ -1,12 +1,12 @@ import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' import { ValueElicitation } from '../../generated/ast.js' import { ValidationAcceptor } from 'langium' -import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' export class ValueElicitationValidationProvider implements ContextMapperValidationProvider { validate (node: ValueElicitation, acceptor: ValidationAcceptor): void { enforceZeroOrOneCardinality(node, 'priority', acceptor) enforceZeroOrOneCardinality(node, 'impact', acceptor) - // TODO: regex enforce consequences + enforceZeroOrOneCardinalityOfListAttribute(node, 'consequences', acceptor) } } diff --git a/src/language/validation/impl/ValueEpicValidationProvider.ts b/src/language/validation/impl/ValueEpicValidationProvider.ts index 40cccb8..476ec8e 100644 --- a/src/language/validation/impl/ValueEpicValidationProvider.ts +++ b/src/language/validation/impl/ValueEpicValidationProvider.ts @@ -4,7 +4,18 @@ import { ValidationAcceptor } from 'langium' export class ValueEpicValidationProvider implements ContextMapperValidationProvider { validate (node: ValueEpic, acceptor: ValidationAcceptor): void { - // TODO: regex enforce realizedValues - // TODO: regex enforce reducedValues + if (node.reducedValues.length === 0) { + acceptor('error', 'At least one reduced value is required', { + node, + property: 'reducedValues' + }) + } + + if (node.realizedValues.length === 0) { + acceptor('error', 'At least one realized value is required', { + node, + property: 'realizedValues' + }) + } } } diff --git a/src/language/validation/impl/ValueValidationProvider.ts b/src/language/validation/impl/ValueValidationProvider.ts index b09be51..790df3a 100644 --- a/src/language/validation/impl/ValueValidationProvider.ts +++ b/src/language/validation/impl/ValueValidationProvider.ts @@ -5,6 +5,6 @@ import { enforceZeroOrOneCardinality } from '../ValidationHelper.js' export class ValueValidationProvider implements ContextMapperValidationProvider { validate (node: Value, acceptor: ValidationAcceptor): void { - enforceZeroOrOneCardinality(node, 'coreValue', acceptor, 'isCore') + enforceZeroOrOneCardinality(node, 'coreValue', acceptor, ['isCore']) } } diff --git a/test/validation/AggregateValidator.test.ts b/test/validation/AggregateValidator.test.ts index d649c6b..e2b0a9e 100644 --- a/test/validation/AggregateValidator.test.ts +++ b/test/validation/AggregateValidator.test.ts @@ -282,7 +282,192 @@ describe('AggregateValidationProvider tests', () => { } `) + expect(document.diagnostics).toHaveLength(2) + expect(document.diagnostics!.map(d => d.range.start.line).sort()).toEqual([3, 4]) + }) + + test('accept one responsibilities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + responsibilities "resp1", "resp2" + } + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple responsibilities', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + responsibilities "resp1", "resp2" + responsibilities "resp3" + } + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one useCases', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + useCases TestCase, TestCaseTwo + } + } + + UseCase TestCase + UseCase TestCaseTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple useCases', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + useCases TestCase, TestCaseTwo + useCases TestCaseThree + } + } + + UseCase TestCase + UseCase TestCaseTwo + UseCase TestCaseThree + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one userStories', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + userStories TestStory, UserStoryTwo + } + } + + UserStory TestStory + UserStory UserStoryTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple userStories', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + userStories TestStory, UserStoryTwo + userStories UserStoryThree + } + } + + UserStory TestStory + UserStory UserStoryTwo + UserStory UserStoryThree + `) + expect(document.diagnostics).toHaveLength(1) expect(document.diagnostics![0].range.start.line).toEqual(3) }) + + test('accept one features', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + features Feature, FeatureTwo + } + } + + UseCase Feature + UserStory FeatureTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple features', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + features Feature, FeatureTwo + features FeatureThree + } + } + + UseCase Feature + UserStory FeatureTwo + UserStory FeatureThree + `) + }) + + test('accept one userRequirements', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + userRequirements Feature, FeatureTwo + } + } + + UseCase Feature + UserStory FeatureTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple userRequirements', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + userRequirements Feature, FeatureTwo + userRequirements FeatureThree + } + } + + UseCase Feature + UserStory FeatureTwo + UserStory FeatureThree + `) + }) + + test('report userRequirements and features', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + userRequirements Feature, FeatureTwo + features FeatureThree + } + } + + UseCase Feature + UserStory FeatureTwo + UserStory FeatureThree + `) + }) + + test('report userRequirements and useCases and userStories', async () => { + document = await parse(` + BoundedContext TestOwner { + Aggregate TestAggregate { + useCases TestCase + userStories TestStory + userRequirements Feature + } + } + UseCase Feature + UseCase TestCase + UserStory TestStory + `) + + expect(document.diagnostics).toHaveLength(3) + expect(document.diagnostics!.map(d => d.range.start.line).sort()).toEqual([3, 4, 5]) + }) }) diff --git a/test/validation/BoundedContextValidator.test.ts b/test/validation/BoundedContextValidator.test.ts index f7c5885..ded6cef 100644 --- a/test/validation/BoundedContextValidator.test.ts +++ b/test/validation/BoundedContextValidator.test.ts @@ -15,26 +15,70 @@ beforeAll(async () => { }) describe('BoundedContextValidationProvider tests', () => { - // TODO: test implementedDomainParts & realizedBoundedContexts after regex enforcement impl + test('accept one implements', async () => { + document = await parse(` + BoundedContext FirstContext implements TestDomain, TestDomainTwo + Domain TestDomain + Domain TestDomainTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple implements', async () => { + document = await parse(` + BoundedContext FirstContext + implements TestDomain, TestDomainTwo + implements OtherDomain + Domain TestDomain + Domain TestDomainTwo + Domain OtherDomain + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) - test('accept one refinement', async () => { + test('accept one realizes', async () => { document = await parse(` BoundedContext FirstContext - refines OtherContext { - - } + realizes OtherContext, OtherContextTwo + BoundedContext OtherContext + BoundedContext OtherContextTwo + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple realizes', async () => { + document = await parse(` + BoundedContext FirstContext + realizes OtherContext, OtherContextTwo + realizes OtherContextThree + BoundedContext OtherContext + BoundedContext OtherContextTwo + BoundedContext OtherContextThree + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one refines', async () => { + document = await parse(` + BoundedContext FirstContext + refines OtherContext BoundedContext OtherContext `) expect(document.diagnostics).toHaveLength(0) }) - test('report multiple refinements', async () => { + test('report multiple refines', async () => { document = await parse(` BoundedContext FirstContext refines OtherContext - refines ThirdContext { - } + refines ThirdContext BoundedContext OtherContext BoundedContext ThirdContext `) @@ -176,4 +220,26 @@ describe('BoundedContextValidationProvider tests', () => { expect(document.diagnostics).toHaveLength(1) expect(document.diagnostics![0].range.start.line).toEqual(2) }) + + test('accept one responsibilities', async () => { + document = await parse(` + BoundedContext FirstContext { + responsibilities "resp1", "resp2" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple responsibilities', async () => { + document = await parse(` + BoundedContext FirstContext { + responsibilities "resp1", "resp2" + responsibilities "resp3" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) }) diff --git a/test/validation/UpstreamDownstreamRelationshipValidator.test.ts b/test/validation/UpstreamDownstreamRelationshipValidator.test.ts new file mode 100644 index 0000000..19a5da1 --- /dev/null +++ b/test/validation/UpstreamDownstreamRelationshipValidator.test.ts @@ -0,0 +1,116 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('UpstreamDownstreamRelationshipValidationProvider tests', () => { + test('accept one implementationTechnology', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + implementationTechnology "java" + } + } + BoundedContext FirstContext + BoundedContext SecondContext + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('accept one implementationTechnology', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + implementationTechnology "java" + implementationTechnology "c#" + } + } + BoundedContext FirstContext + BoundedContext SecondContext + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one exposedAggregates', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + exposedAggregates FirstAggregate, SecondAggregate + } + } + BoundedContext FirstContext { + Aggregate FirstAggregate + } + BoundedContext SecondContext { + Aggregate SecondAggregate + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple exposedAggregates', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + exposedAggregates FirstAggregate, SecondAggregate + exposedAggregates ThirdAggregate + } + } + BoundedContext FirstContext { + Aggregate FirstAggregate + } + BoundedContext SecondContext { + Aggregate SecondAggregate + Aggregate ThirdAggregate + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) + + test('accept one downstreamRights', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + downstreamRights INFLUENCER + } + } + BoundedContext FirstContext + BoundedContext SecondContext + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple downstreamRights', async () => { + document = await parse(` + ContextMap { + FirstContext -> SecondContext { + downstreamRights INFLUENCER + downstreamRights INFLUENCER + } + } + BoundedContext FirstContext + BoundedContext SecondContext + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(3) + }) +}) diff --git a/test/validation/UseCaseValidator.test.ts b/test/validation/UseCaseValidator.test.ts index 69c591a..9f40670 100644 --- a/test/validation/UseCaseValidator.test.ts +++ b/test/validation/UseCaseValidator.test.ts @@ -102,4 +102,53 @@ describe('UseCaseValidationProvider tests', () => { expect(document.diagnostics).toHaveLength(1) expect(document.diagnostics![0].range.start.line).toEqual(2) }) + + test('accept one secondaryActors', async () => { + document = await parse(` + UseCase TestUseCase { + secondaryActors "act1", "act2" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple secondaryActors', async () => { + document = await parse(` + UseCase TestUseCase { + secondaryActors "act1", "act2" + secondaryActors "act3" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('accept one interactions', async () => { + document = await parse(` + UseCase TestUseCase { + interactions + create an "order", + update an "order" + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('accept multiple interactions', async () => { + document = await parse(` + UseCase TestUseCase { + interactions + create an "order", + update an "order" + interactions + delete an "order" + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) }) diff --git a/test/validation/ValidationHelper.test.ts b/test/validation/ValidationHelper.test.ts new file mode 100644 index 0000000..32d8a1c --- /dev/null +++ b/test/validation/ValidationHelper.test.ts @@ -0,0 +1,118 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { ValidationAcceptor, AstNode } from 'langium' +import { + enforceZeroOrOneCardinality, + enforceZeroOrOneCardinalityOfListAttribute +} from '../../src/language/validation/ValidationHelper.js' + +export interface TestNode extends AstNode { + key: string[] +} + +export interface NonArrayNode extends AstNode { + key: string +} + +let acceptor: ValidationAcceptor + +beforeEach(() => { + acceptor = vi.fn() +}) + +afterEach(() => { + vi.resetAllMocks() +}) + +describe('ValidationHelper tests', () => { + test('enforceZeroOrOneCardinality with non-array', () => { + const node = { + key: 'abc' + } as NonArrayNode + + enforceZeroOrOneCardinality(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(1) + }) + + test('enforceZeroOrOneCardinality with empty array', () => { + const node = { + key: [] as string[] + } as TestNode + + enforceZeroOrOneCardinality(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(0) + }) + + test('enforceZeroOrOneCardinality with array of length 1', () => { + const node = { + key: ['test'] + } as TestNode + + enforceZeroOrOneCardinality(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(0) + }) + + test('enforceZeroOrOneCardinality with array of length 2', () => { + const node = { + key: ['test', 'test2'] + } as TestNode + + enforceZeroOrOneCardinality(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(1) + }) + + test('enforceZeroOrOneCardinalityOfListAttribute with no match', () => { + const node: TestNode = { + $cstNode: { + text: ` + Test Structure { + test "test" + } + ` + }, + key: [] as string[] + } as TestNode + + enforceZeroOrOneCardinalityOfListAttribute(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(0) + }) + + test('enforceZeroOrOneCardinalityOfListAttribute with one match', () => { + const node: TestNode = { + $cstNode: { + text: ` + Test Structurekey { + key "test", "key" + } + ` + }, + key: ['test', 'key'] + } as TestNode + + enforceZeroOrOneCardinalityOfListAttribute(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(0) + }) + + test('enforceZeroOrOneCardinalityOfListAttribute with two matches', () => { + const node: TestNode = { + $cstNode: { + text: ` + Test Structurekey { + key "test", "key" + key "test2", "key2" + } + ` + }, + key: ['test', 'key', 'test2', 'key2'] + } as TestNode + + enforceZeroOrOneCardinalityOfListAttribute(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(1) + }) +}) diff --git a/test/validation/ValueElicitationValidator.test.ts b/test/validation/ValueElicitationValidator.test.ts index 1ee6928..549b79e 100644 --- a/test/validation/ValueElicitationValidator.test.ts +++ b/test/validation/ValueElicitationValidator.test.ts @@ -105,4 +105,45 @@ describe('ValueElicitationValidationProvider tests', () => { expect(document.diagnostics).toHaveLength(1) expect(document.diagnostics![0].range.start.line).toEqual(4) }) + + test('report one consequences', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + consequences + good "conseq" + bad "conseq" + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) + + test('report multiple impacts', async () => { + document = await parse(` + ValueRegister TestRegister { + Value TestValue { + Stakeholder TestStakeholder { + consequences + good "conseq" + bad "conseq" + consequences + neutral "conseq" + } + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(4) + }) }) diff --git a/test/validation/ValueEpicValidator.test.ts b/test/validation/ValueEpicValidator.test.ts new file mode 100644 index 0000000..abbba38 --- /dev/null +++ b/test/validation/ValueEpicValidator.test.ts @@ -0,0 +1,83 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { beforeAll, describe, expect, test } from 'vitest' + +let services: ReturnType +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + const doParse = parseHelper(services.ContextMapperDsl) + parse = (input: string) => doParse(input, { validation: true }) +}) + +describe('ValueEpicValidationProvider tests', () => { + test('report missing reduction of', async () => { + document = await parse(` + ValueRegister TestRegister { + ValueEpic TestEpic { + As a TestStakeholder I value "val" as demonstrated in + realization of "TestRealization" + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('report missing realization of', async () => { + document = await parse(` + ValueRegister TestRegister { + ValueEpic TestEpic { + As a TestStakeholder I value "val" as demonstrated in + reduction of "reduc" + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(1) + expect(document.diagnostics![0].range.start.line).toEqual(2) + }) + + test('report missing realization & reduction of', async () => { + document = await parse(` + ValueRegister TestRegister { + ValueEpic TestEpic { + As a TestStakeholder I value "val" as demonstrated in + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(2) + }) + + test('accept epic with realization & reduction', async () => { + document = await parse(` + ValueRegister TestRegister { + ValueEpic TestEpic { + As a TestStakeholder I value "val" as demonstrated in + reduction of "reduc" + realization of "real" + } + } + Stakeholders { + Stakeholder TestStakeholder + } + `) + + expect(document.diagnostics).toHaveLength(0) + }) +}) From cc8c881bf52b7e48a8e5e637bcb323daf554d3f6 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Fri, 2 May 2025 13:01:54 +0200 Subject: [PATCH 7/9] update validation diagram --- docs/class_semantic_validation.puml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/class_semantic_validation.puml b/docs/class_semantic_validation.puml index b517b8e..4e8621c 100644 --- a/docs/class_semantic_validation.puml +++ b/docs/class_semantic_validation.puml @@ -7,21 +7,21 @@ class ContextMapperDslValidationRegistry { } class ContextMapperDslValidator { -+ checkContextMappingModel(ContextMappingModel, ValidationAcceptor) -+ checkValue(Value, ValidationAcceptor) ++ validate(AstNode, ValidationAcceptor) } class ContextMapperValidationProviderRegistry { +- _providers: Map> + get(AstNode): ContextMapperValidationProvider } interface ContextMapperValidationProvider { -+ supports(AstNode): boolean + validate(T, ValidationAcceptor) } ValidationRegistry <|-- ContextMapperDslValidationRegistry ContextMapperDslValidationRegistry --> ContextMapperDslValidator +ContextMapperDslValidationRegistry --> ContextMapperValidationProviderRegistry ContextMapperDslValidator --> ContextMapperValidationProviderRegistry ContextMapperValidationProviderRegistry o-- "0..*" ContextMapperValidationProvider @enduml \ No newline at end of file From 95e06871865807168def3505c571b36f61e9b5ba Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Fri, 2 May 2025 13:35:52 +0200 Subject: [PATCH 8/9] fix wording --- src/language/references/ContextMapperDslScopeComputation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/references/ContextMapperDslScopeComputation.ts b/src/language/references/ContextMapperDslScopeComputation.ts index be92523..cd153ab 100644 --- a/src/language/references/ContextMapperDslScopeComputation.ts +++ b/src/language/references/ContextMapperDslScopeComputation.ts @@ -5,7 +5,7 @@ export class ContextMapperDslScopeComputation extends DefaultScopeComputation { /* For the time being imports aren't supported yet. By default, Langium adds top-level elements to the global scope. - Without this behavior is wrong and therefore no nodes must be exported here + Without imports, this behavior is wrong and therefore no nodes must be exported here. */ override computeExportsForNode ( parentNode: AstNode, From ed8eb4f41c6c7aa7292ae6e487f90cd4229c5e8f Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Fri, 2 May 2025 13:44:19 +0200 Subject: [PATCH 9/9] fix qodana findings --- .../ContextMapperDslScopeComputation.ts | 8 ++-- test/validation/ValidationHelper.test.ts | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/language/references/ContextMapperDslScopeComputation.ts b/src/language/references/ContextMapperDslScopeComputation.ts index cd153ab..6387708 100644 --- a/src/language/references/ContextMapperDslScopeComputation.ts +++ b/src/language/references/ContextMapperDslScopeComputation.ts @@ -8,10 +8,10 @@ export class ContextMapperDslScopeComputation extends DefaultScopeComputation { Without imports, this behavior is wrong and therefore no nodes must be exported here. */ override computeExportsForNode ( - parentNode: AstNode, - document: LangiumDocument, - children?: (root: AstNode) => Iterable, - cancelToken?: CancellationToken + _parentNode: AstNode, + _document: LangiumDocument, + _children?: (root: AstNode) => Iterable, + _cancelToken?: CancellationToken ): Promise { return Promise.resolve([]) } diff --git a/test/validation/ValidationHelper.test.ts b/test/validation/ValidationHelper.test.ts index 32d8a1c..f6a8b66 100644 --- a/test/validation/ValidationHelper.test.ts +++ b/test/validation/ValidationHelper.test.ts @@ -64,6 +64,16 @@ describe('ValidationHelper tests', () => { expect(acceptor).toHaveBeenCalledTimes(1) }) + test('enforceZeroOrOneCardinality with multiple keywords', () => { + const node = { + key: ['test', 'test2'] + } as TestNode + + enforceZeroOrOneCardinality(node, 'key', acceptor, ['key', 'keyx']) + + expect(acceptor).toHaveBeenCalledTimes(2) + }) + test('enforceZeroOrOneCardinalityOfListAttribute with no match', () => { const node: TestNode = { $cstNode: { @@ -115,4 +125,32 @@ describe('ValidationHelper tests', () => { expect(acceptor).toHaveBeenCalledTimes(1) }) + + test('enforceZeroOrOneCardinalityOfListAttribute with multiple keywords', () => { + const node: TestNode = { + $cstNode: { + text: ` + Test Structurekey { + key "test", "key" + keyx "test2", "key2" + } + ` + }, + key: ['test', 'key', 'test2', 'key2'] + } as TestNode + + enforceZeroOrOneCardinalityOfListAttribute(node, 'key', acceptor, ['key', 'keyx']) + + expect(acceptor).toHaveBeenCalledTimes(2) + }) + + test('enforceZeroOrOneCardinalityOfListAttribute with non-array', () => { + const node = { + key: 'abc' + } as NonArrayNode + + enforceZeroOrOneCardinalityOfListAttribute(node, 'key', acceptor) + + expect(acceptor).toHaveBeenCalledTimes(1) + }) })