Skip to content

Enforce and validate your model

Sometimes you may want to enforce custom rules on your model or validate its consistency.
Here’s a simple recipe for how to do that.

In this example we’ll use Vitest together with the LikeC4 API.
The LikeC4 API gives you methods to query and traverse the model - perfect for writing tests that enforce your rules.

npm i likec4 vitest

Suppose we want to enforce that every element of kind app has a technology specified.

test/metadata.spec.ts
import { LikeC4 } from './LikeC4'
import { test } from 'vitest'
// Initialize and compute LikeC4 Model
const likec4 = await LikeC4.fromWorkspace('..')
const model = likec4.computedModel()
// With `test.for` we generate tests for each element of kind `app`
// This improves the output, showing each test failure separately
test.for(
model
// Select elements of kind `app`
.elementsWhere({ kind: 'app' })
// Map to array of [id, element] tuples, we need it for test names
.map(e => [e.id, e] as const)
.toArray(),
)('app "%s" has technology', ([, e], { expect }) => {
expect(e.technology).toBeTruthy()
})
// Or we can use `expect.soft` to accumulate all errors
test('elements of kind `app` have technology', ({ expect }) => {
expect.hasAssertions()
for (const app of model.elements()) {
if (app.kind !== 'app') continue // Skip non-app elements
expect.soft(app.technology, `app ${app.id} has no technology`).toBeTruthy()
}
})

We can optimize our tests by pre-generating the model in global setup:

global-setup.ts
import { spawn } from 'node:child_process'
export default async function() {
await spawn('likec4', [
'gen',
'model',
'--output',
'./test/likec4-model.ts',
'./src'
])
return async () => {
}
}

The generated model is fully typed, giving us type checking and autocompletion in tests:

test/metadata.spec.ts
import { likec4model } from './likec4-model'
import { test } from 'vitest'
test('Relationships should have metadata', ({ expect }) => {
expect.hasAssertions()
for (const r of likec4model.relationships()) {
expect.soft(
r.getMetadata('key'), // here we get type checking
`Relationship ${r.source.id} -> ${r.target.id} has no metadata`
).toBeDefined()
}
})

We can go further and use test context to improve our experience:

test/likec4test.ts
import { likec4model } from './likec4-model'
import { test } from 'vitest'
interface LikeC4TestFixtures {
likec4: typeof likec4model
}
// This wil be our test function with the model in the context
export const likec4test = test.extend<LikeC4TestFixtures>({
likec4: async ({}, use) => {
await use(likec4model)
},
})

Now refactor tests to use it:

test/metadata.spec.ts
import { likec4test } from './likec4test'
likec4test('Relationships should have metadata', ({ expect, likec4 }) => {
expect.hasAssertions()
for (const r of likec4.relationships()) {
expect.soft(
r.getMetadata('key'), // here we get type checking
`Relationship ${r.source.id} -> ${r.target.id} has no metadata`
).toBeDefined()
}
})

This approach makes it easy to enforce custom constraints and validate your model consistency.
Running these checks in CI pipeline is fast and provides immediate feedback when the model breaks your rules.