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 diff --git a/src/language/ContextMapperDslModule.ts b/src/language/ContextMapperDslModule.ts index e1e6244..c763e94 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. @@ -45,14 +46,15 @@ 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), FoldingRangeProvider: (services) => new ContextMapperDslFoldingRangeProvider(services) }, references: { - ScopeProvider: (services) => new ContextMapperDslScopeProvider(services) + ScopeProvider: (services) => new ContextMapperDslScopeProvider(services), + ScopeComputation: (services) => new ContextMapperDslScopeComputation(services) } } diff --git a/src/language/context-mapper-dsl.langium b/src/language/context-mapper-dsl.langium index e6f73ce..de90da0 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,21 @@ Aggregate: (doc=STRING)? "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])*) - ) & - ('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) - ) + (('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) + )* CLOSE)? ; @@ -205,13 +201,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 +243,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 +282,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 +330,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 +343,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/references/ContextMapperDslScopeComputation.ts b/src/language/references/ContextMapperDslScopeComputation.ts new file mode 100644 index 0000000..6387708 --- /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 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 + ): Promise { + return Promise.resolve([]) + } +} 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..a7f81d5 100644 --- a/src/language/validation/ContextMapperValidationProviderRegistry.ts +++ b/src/language/validation/ContextMapperValidationProviderRegistry.ts @@ -1,15 +1,54 @@ 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, ValueEpic +} 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' +import { ValueEpicValidationProvider } from './impl/ValueEpicValidationProvider.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()], + [ValueEpic, new ValueEpicValidationProvider()] + ]) 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..2fdd8ec --- /dev/null +++ b/src/language/validation/ValidationHelper.ts @@ -0,0 +1,57 @@ +import { AstNode, ValidationAcceptor } from 'langium' + +export function enforceZeroOrOneCardinality (node: AstNode, property: string, acceptor: ValidationAcceptor, keywords: string[] = [property]) { + const nodeProperty = node[property as keyof AstNode] + 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) { + 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/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..a199c36 --- /dev/null +++ b/src/language/validation/impl/AggregateValidationProvider.ts @@ -0,0 +1,38 @@ +import { ContextMapperValidationProvider } from '../ContextMapperValidationProvider.js' +import { Aggregate } from '../../generated/ast.js' +import { Properties, ValidationAcceptor } from 'langium' +import { enforceZeroOrOneCardinality, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' + +export class AggregateValidationProvider implements ContextMapperValidationProvider { + validate (node: Aggregate, acceptor: ValidationAcceptor): void { + 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, ['likelihoodForChange', 'structuralVolatility']) + 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) + + // 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 new file mode 100644 index 0000000..068f184 --- /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, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' + +export class BoundedContextValidationProvider implements ContextMapperValidationProvider { + validate (node: BoundedContext, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinalityOfListAttribute(node, 'implementedDomainParts', acceptor, ['implements']) + enforceZeroOrOneCardinalityOfListAttribute(node, 'realizedBoundedContexts', acceptor, ['realizes']) + enforceZeroOrOneCardinality(node, 'refinedBoundedContext', acceptor, ['refines']) + + enforceZeroOrOneCardinality(node, 'domainVisionStatement', acceptor) + enforceZeroOrOneCardinality(node, 'type', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'responsibilities', acceptor) + 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..e6a4412 --- /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, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' + +export class UpstreamDownstreamRelationshipValidationProvider implements ContextMapperValidationProvider { + validate (node: UpstreamDownstreamRelationship, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'implementationTechnology', 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 new file mode 100644 index 0000000..7480e42 --- /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, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' + +export class UseCaseValidationProvider implements ContextMapperValidationProvider { + validate (node: UserRequirement, acceptor: ValidationAcceptor): void { + if (isUseCase(node)) { + 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 new file mode 100644 index 0000000..88301c9 --- /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, enforceZeroOrOneCardinalityOfListAttribute } from '../ValidationHelper.js' + +export class ValueElicitationValidationProvider implements ContextMapperValidationProvider { + validate (node: ValueElicitation, acceptor: ValidationAcceptor): void { + enforceZeroOrOneCardinality(node, 'priority', acceptor) + enforceZeroOrOneCardinality(node, 'impact', acceptor) + enforceZeroOrOneCardinalityOfListAttribute(node, 'consequences', acceptor) + } +} diff --git a/src/language/validation/impl/ValueEpicValidationProvider.ts b/src/language/validation/impl/ValueEpicValidationProvider.ts new file mode 100644 index 0000000..476ec8e --- /dev/null +++ b/src/language/validation/impl/ValueEpicValidationProvider.ts @@ -0,0 +1,21 @@ +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 { + 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 new file mode 100644 index 0000000..790df3a --- /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/completion/Completion.test.ts b/test/completion/Completion.test.ts new file mode 100644 index 0000000..d507c5b --- /dev/null +++ b/test/completion/Completion.test.ts @@ -0,0 +1,99 @@ +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' +import { uinteger } from 'vscode-languageserver-types' + +let services: ReturnType +let completionProvider: CompletionProvider +let parse: ReturnType> + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + completionProvider = services.ContextMapperDsl.lsp.CompletionProvider! + parse = parseHelper(services.ContextMapperDsl) +}) + +afterEach(async () => { + await clearDocuments(services.shared, services.shared.workspace.LangiumDocuments.all.toArray()) +}) + +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 = 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 + } + } +} 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..e2b0a9e --- /dev/null +++ b/test/validation/AggregateValidator.test.ts @@ -0,0 +1,473 @@ +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(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 new file mode 100644 index 0000000..ded6cef --- /dev/null +++ b/test/validation/BoundedContextValidator.test.ts @@ -0,0 +1,245 @@ +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', () => { + 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 realizes', async () => { + document = await parse(` + BoundedContext FirstContext + 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 refines', 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) + }) + + 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/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/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 new file mode 100644 index 0000000..9f40670 --- /dev/null +++ b/test/validation/UseCaseValidator.test.ts @@ -0,0 +1,154 @@ +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) + }) + + 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..f6a8b66 --- /dev/null +++ b/test/validation/ValidationHelper.test.ts @@ -0,0 +1,156 @@ +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('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: { + 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) + }) + + 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) + }) +}) diff --git a/test/validation/ValueElicitationValidator.test.ts b/test/validation/ValueElicitationValidator.test.ts new file mode 100644 index 0000000..549b79e --- /dev/null +++ b/test/validation/ValueElicitationValidator.test.ts @@ -0,0 +1,149 @@ +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) + }) + + 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) + }) +}) 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', () => {