@likec4/generators 1.52.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ import type { Fqn } from '@likec4/core/types'
2
+ import { invariant, nameFromFqn, parentFqn, sortParentsFirst } from '@likec4/core/utils'
3
+ import { isEmptyish, pipe, values } from 'remeda'
4
+
5
+ import { schemas } from '../schemas'
6
+ import {
7
+ type AnyOp,
8
+ type Ctx,
9
+ body,
10
+ foreach,
11
+ inlineText,
12
+ lines,
13
+ print,
14
+ printProperty,
15
+ property,
16
+ select,
17
+ spaceBetween,
18
+ when,
19
+ withctx,
20
+ zodOp,
21
+ } from './base'
22
+ import { fqnRef } from './expressions'
23
+ import {
24
+ colorProperty,
25
+ descriptionProperty,
26
+ linksProperty,
27
+ metadataProperty,
28
+ styleProperties,
29
+ summaryProperty,
30
+ tagsProperty,
31
+ technologyProperty,
32
+ } from './properties'
33
+
34
+ // --- Tree building ---
35
+
36
+ type NodeData = schemas.deployment.node.Data
37
+ type InstanceData = schemas.deployment.instance.Data
38
+ type ElementData = schemas.deployment.element.Data
39
+
40
+ type TreeNodeData =
41
+ | NodeData & {
42
+ children: Array<TreeNodeData>
43
+ }
44
+ | InstanceData
45
+
46
+ function buildTree(elements: ElementData[]): {
47
+ roots: readonly TreeNodeData[]
48
+ nodes: ReadonlyMap<Fqn, TreeNodeData>
49
+ exists: (fqn: Fqn) => boolean
50
+ } {
51
+ const nodes = new Map<Fqn, TreeNodeData>()
52
+ const roots: TreeNodeData[] = []
53
+ const sorted = pipe(
54
+ elements,
55
+ sortParentsFirst,
56
+ )
57
+
58
+ for (const element of sorted) {
59
+ let node: TreeNodeData
60
+ if ('element' in element) {
61
+ node = element
62
+ } else {
63
+ node = {
64
+ ...element,
65
+ children: [],
66
+ }
67
+ }
68
+ nodes.set(element.id, node)
69
+
70
+ const parentId = parentFqn(element.id)
71
+ const parent = parentId ? nodes.get(parentId) : undefined
72
+
73
+ if (parent && 'children' in parent) {
74
+ parent.children.push(node)
75
+ } else {
76
+ roots.push(node)
77
+ }
78
+ }
79
+
80
+ return {
81
+ roots,
82
+ nodes,
83
+ exists: (fqn: Fqn) => nodes.has(fqn),
84
+ }
85
+ }
86
+
87
+ // --- Predicates ---
88
+
89
+ function hasStyleProps(el: NodeData | InstanceData): boolean {
90
+ return !isEmptyish(el.style)
91
+ }
92
+
93
+ function hasElementProps(el: ElementData): boolean {
94
+ return !!(
95
+ el.description || el.summary || el.technology || el.notation
96
+ || (el.tags && el.tags.length > 0)
97
+ || (el.links && el.links.length > 0)
98
+ || !isEmptyish(el.metadata)
99
+ || hasStyleProps(el)
100
+ )
101
+ }
102
+
103
+ // --- Element ---
104
+
105
+ const elementProperties = zodOp(schemas.deployment.element)(
106
+ lines(
107
+ tagsProperty(),
108
+ technologyProperty(),
109
+ summaryProperty(),
110
+ descriptionProperty(),
111
+ linksProperty(),
112
+ metadataProperty(),
113
+ select(
114
+ e => hasStyleProps(e) ? e.style : undefined,
115
+ body('style')(
116
+ styleProperties(),
117
+ ),
118
+ ),
119
+ ),
120
+ )
121
+
122
+ export const instance = zodOp(schemas.deployment.instance)(
123
+ spaceBetween(
124
+ when(
125
+ v => nameFromFqn(v.id) !== nameFromFqn(v.element),
126
+ print(v => nameFromFqn(v.id)),
127
+ print(' ='),
128
+ ),
129
+ print('instanceOf'),
130
+ printProperty('element'),
131
+ when(
132
+ v => !!v.title && v.title !== nameFromFqn(v.id),
133
+ property('title', inlineText()),
134
+ ),
135
+ when(
136
+ e => hasElementProps(e),
137
+ body(
138
+ elementProperties(),
139
+ ),
140
+ ),
141
+ ),
142
+ )
143
+
144
+ export function node() {
145
+ return function nodeOp<E extends TreeNodeData>(
146
+ { ctx, out }: Ctx<E>,
147
+ ): Ctx<E> {
148
+ const el = ctx
149
+
150
+ if ('element' in el) {
151
+ instance()({ ctx: el, out })
152
+ return { ctx, out }
153
+ }
154
+ invariant('children' in el, 'Node must have children property')
155
+ const needsBody = el.children.length > 0 || hasElementProps(el)
156
+
157
+ const name = nameFromFqn(el.id)
158
+
159
+ const inline: AnyOp[] = [
160
+ print(name),
161
+ print('='),
162
+ print(el.kind),
163
+ ]
164
+
165
+ if (el.title && el.title !== name) {
166
+ inline.push(inlineText(el.title))
167
+ }
168
+
169
+ if (needsBody) {
170
+ inline.push(
171
+ body(
172
+ lines(2)(
173
+ withctx(el, elementProperties()),
174
+ ...el.children.map(node => withctx(node, nodeOp)),
175
+ ),
176
+ ),
177
+ )
178
+ }
179
+
180
+ return spaceBetween(...inline)({ ctx, out })
181
+ }
182
+ }
183
+
184
+ // --- Relationship ---
185
+
186
+ function hasRelationStyle(rel: schemas.deployment.relationship.Data): boolean {
187
+ return !!(
188
+ rel.color || rel.line || rel.head || rel.tail
189
+ )
190
+ }
191
+
192
+ function hasRelationProps(rel: schemas.deployment.relationship.Data): boolean {
193
+ return !!(
194
+ rel.description || rel.summary || rel.technology
195
+ || (rel.tags && rel.tags.length > 0)
196
+ || (rel.links && rel.links.length > 0)
197
+ || !isEmptyish(rel.metadata)
198
+ || hasRelationStyle(rel)
199
+ || rel.navigateTo
200
+ )
201
+ }
202
+
203
+ export const relationship = zodOp(schemas.deployment.relationship)(
204
+ spaceBetween(
205
+ property('source', fqnRef()),
206
+ print(rel => rel.kind ? `-[${rel.kind}]->` : '->'),
207
+ property('target', fqnRef()),
208
+ property(
209
+ 'title',
210
+ inlineText(),
211
+ ),
212
+ when(
213
+ hasRelationProps,
214
+ body(
215
+ tagsProperty(),
216
+ technologyProperty(),
217
+ summaryProperty(),
218
+ descriptionProperty(),
219
+ property('navigateTo'),
220
+ linksProperty(),
221
+ metadataProperty(),
222
+ when(
223
+ hasRelationStyle,
224
+ body('style')(
225
+ colorProperty(),
226
+ property('line'),
227
+ property('head'),
228
+ property('tail'),
229
+ ),
230
+ ),
231
+ ),
232
+ ),
233
+ ),
234
+ )
235
+
236
+ // export const element = zodOp(schemas.model.element)(
237
+ // elementTree(),
238
+ // )
239
+
240
+ // --- Main ---
241
+
242
+ export const deployment = zodOp(schemas.deployment.schema)(
243
+ body('deployment')(
244
+ lines(2)(
245
+ select(
246
+ d => buildTree(d.elements ? values(d.elements) : []).roots,
247
+ lines(2)(
248
+ foreach(
249
+ node(),
250
+ ),
251
+ ),
252
+ ),
253
+ select(
254
+ d => d.relations ? values(d.relations) : undefined,
255
+ lines(2)(
256
+ foreach(
257
+ relationship(),
258
+ ),
259
+ ),
260
+ ),
261
+ ),
262
+ ),
263
+ )
@@ -0,0 +1,422 @@
1
+ import { type PredicateSelector, nonexhaustive } from '@likec4/core'
2
+ import { joinToNode } from 'langium/generate'
3
+ import { map } from 'remeda'
4
+ import * as schemas from '../schemas/expression'
5
+ import {
6
+ type Output,
7
+ body,
8
+ executeOnFresh,
9
+ fresh,
10
+ indent,
11
+ lazy,
12
+ merge,
13
+ print,
14
+ property,
15
+ space,
16
+ spaceBetween,
17
+ withctx,
18
+ zodOp,
19
+ } from './base'
20
+ import {
21
+ descriptionProperty,
22
+ markdownProperty,
23
+ notationProperty,
24
+ notesProperty,
25
+ styleProperties,
26
+ titleProperty,
27
+ } from './properties'
28
+
29
+ function appendSelector(out: Output, selector: PredicateSelector | undefined) {
30
+ if (selector) {
31
+ switch (selector) {
32
+ case 'children':
33
+ out.append('.*')
34
+ break
35
+ case 'descendants':
36
+ out.append('.**')
37
+ break
38
+ case 'expanded':
39
+ out.append('._')
40
+ break
41
+ default:
42
+ nonexhaustive(selector)
43
+ }
44
+ }
45
+ return out
46
+ }
47
+
48
+ export const whereTagEqual = zodOp(schemas.whereTag)(function whereTagEqualOp({ ctx: { tag }, out }) {
49
+ if ('eq' in tag) {
50
+ return out.appendTemplate`tag is #${tag.eq}`
51
+ }
52
+ if ('neq' in tag) {
53
+ return out.appendTemplate`tag is not #${tag.neq}`
54
+ }
55
+ nonexhaustive(tag)
56
+ })
57
+
58
+ export const whereKindEqual = zodOp(schemas.whereKind)(function whereKindEqualOp({ ctx: { kind }, out }) {
59
+ if ('eq' in kind) {
60
+ return out.appendTemplate`kind is ${kind.eq}`
61
+ }
62
+ if ('neq' in kind) {
63
+ return out.appendTemplate`kind is not ${kind.neq}`
64
+ }
65
+ nonexhaustive(kind)
66
+ })
67
+
68
+ function quoteMetadataValue(v: string): string {
69
+ return v === 'true' || v === 'false' ? v : `"${v}"`
70
+ }
71
+
72
+ export const whereMetadataEqual = zodOp(schemas.whereMetadata)(
73
+ function whereMetadataEqualOp({ ctx: { metadata }, out }) {
74
+ const { key, value } = metadata
75
+ if (value === undefined) {
76
+ return out.appendTemplate`metadata.${key}`
77
+ }
78
+ if ('eq' in value) {
79
+ return out.append(`metadata.${key} is ${quoteMetadataValue(value.eq)}`)
80
+ }
81
+ if ('neq' in value) {
82
+ return out.append(`metadata.${key} is not ${quoteMetadataValue(value.neq)}`)
83
+ }
84
+ nonexhaustive(value)
85
+ },
86
+ )
87
+
88
+ export const whereNot = zodOp(schemas.whereNot)(
89
+ property(
90
+ 'not',
91
+ spaceBetween(
92
+ print('not ('),
93
+ lazy(() => whereOperator()),
94
+ print(')'),
95
+ ),
96
+ ),
97
+ )
98
+
99
+ export const whereParticipant = zodOp(schemas.whereParticipant)(
100
+ function whereParticipantOp({ ctx: { participant, operator }, out }) {
101
+ out.append(participant, '.')
102
+ if ('tag' in operator) {
103
+ whereTagEqual()({ ctx: operator, out })
104
+ return
105
+ }
106
+ if ('kind' in operator) {
107
+ whereKindEqual()({ ctx: operator, out })
108
+ return
109
+ }
110
+ if ('metadata' in operator) {
111
+ whereMetadataEqual()({ ctx: operator, out })
112
+ return
113
+ }
114
+ nonexhaustive(operator)
115
+ },
116
+ )
117
+
118
+ export const whereAnd = zodOp(schemas.whereAnd)(function whereAndOp({ ctx: { and }, out }) {
119
+ const operands = map(and, operand => {
120
+ let { out } = executeOnFresh(operand, whereOperator())
121
+ const wrapWithBraces = 'or' in operand
122
+ if (wrapWithBraces) {
123
+ out = fresh().out.append('(', ...out.contents, ')')
124
+ }
125
+ return out
126
+ })
127
+ return out.append(
128
+ joinToNode(operands, {
129
+ appendNewLineIfNotEmpty: true,
130
+ skipNewLineAfterLastItem: true,
131
+ prefix(_element, index) {
132
+ return index > 0 ? 'and ' : undefined
133
+ },
134
+ }),
135
+ )
136
+ })
137
+
138
+ export const whereOr = zodOp(schemas.whereOr)(({ ctx: { or }, out }) => {
139
+ const operands = map(or, operand => {
140
+ let { out } = executeOnFresh(operand, whereOperator())
141
+ const wrapWithBraces = 'and' in operand
142
+ if (wrapWithBraces) {
143
+ out = fresh().out.append('(', ...out.contents, ')')
144
+ }
145
+ return out
146
+ })
147
+ return out.append(
148
+ joinToNode(operands, {
149
+ appendNewLineIfNotEmpty: true,
150
+ skipNewLineAfterLastItem: true,
151
+ prefix(_element, index) {
152
+ return index > 0 ? 'or ' : undefined
153
+ },
154
+ }),
155
+ )
156
+ })
157
+
158
+ export const whereOperator = zodOp(schemas.whereOperator)(({ ctx, exec }) => {
159
+ if ('and' in ctx) {
160
+ return exec(ctx, whereAnd())
161
+ }
162
+ if ('or' in ctx) {
163
+ return exec(ctx, whereOr())
164
+ }
165
+ if ('not' in ctx) {
166
+ return exec(ctx, whereNot())
167
+ }
168
+ if ('tag' in ctx) {
169
+ return exec(ctx, whereTagEqual())
170
+ }
171
+ if ('kind' in ctx) {
172
+ return exec(ctx, whereKindEqual())
173
+ }
174
+ if ('metadata' in ctx) {
175
+ return exec(ctx, whereMetadataEqual())
176
+ }
177
+ if ('participant' in ctx) {
178
+ return exec(ctx, whereParticipant())
179
+ }
180
+ nonexhaustive(ctx)
181
+ })
182
+
183
+ export const fqnRef = zodOp(schemas.fqnRef)(({ ctx, out }) => {
184
+ if ('model' in ctx) {
185
+ out.append(ctx.model)
186
+ } else {
187
+ out.append(ctx.deployment)
188
+ if (ctx.element) {
189
+ out.append('.', ctx.element)
190
+ }
191
+ }
192
+ return out
193
+ })
194
+
195
+ export const fqnExpr = zodOp(schemas.fqnExpr)(({ ctx, out, exec }) => {
196
+ if ('wildcard' in ctx) {
197
+ return out.append('*')
198
+ }
199
+ if ('elementKind' in ctx) {
200
+ return out
201
+ .append('element.kind')
202
+ .append(ctx.isEqual ? ' = ' : ' != ')
203
+ .append(ctx.elementKind)
204
+ }
205
+ if ('elementTag' in ctx) {
206
+ return out
207
+ .append('element.tag')
208
+ .append(ctx.isEqual ? ' = ' : ' != ')
209
+ .append(`#${ctx.elementTag}`)
210
+ }
211
+ if ('ref' in ctx) {
212
+ exec(ctx.ref, fqnRef())
213
+ appendSelector(out, ctx.selector)
214
+ return out
215
+ }
216
+ nonexhaustive(ctx)
217
+ })
218
+
219
+ export const fqnExprCustom = zodOp(schemas.fqnExprCustom)(({ ctx: { custom }, exec }) => {
220
+ exec(
221
+ custom.expr,
222
+ fqnExprOrWhere(),
223
+ )
224
+ const customOp = withctx(custom)(
225
+ body('with')(
226
+ titleProperty(),
227
+ descriptionProperty(),
228
+ notationProperty(),
229
+ notesProperty(),
230
+ property('navigateTo'),
231
+ styleProperties(),
232
+ ),
233
+ )
234
+ if ('where' in custom.expr) {
235
+ return exec(
236
+ {},
237
+ indent(
238
+ customOp,
239
+ ),
240
+ )
241
+ }
242
+ return exec(
243
+ {},
244
+ space(),
245
+ customOp,
246
+ )
247
+ })
248
+
249
+ export const fqnExprOrWhere = zodOp(schemas.fqnExprOrWhere)(({ ctx, exec }) => {
250
+ if ('where' in ctx) {
251
+ exec(ctx.where.expr, fqnExpr())
252
+ exec(
253
+ ctx.where.condition,
254
+ indent(
255
+ print('where'),
256
+ indent(
257
+ whereOperator(),
258
+ ),
259
+ ),
260
+ )
261
+ return
262
+ }
263
+ exec(ctx, fqnExpr())
264
+ })
265
+
266
+ export const fqnExprAny = zodOp(schemas.fqnExprAny)(({ ctx, out }) => {
267
+ if ('custom' in ctx) {
268
+ return fqnExprCustom()({ ctx, out })
269
+ }
270
+ return fqnExprOrWhere()({ ctx, out })
271
+ })
272
+
273
+ // ──────────────────────────────────────────────
274
+ // RelationExpr operators (expression.ts types)
275
+ // ──────────────────────────────────────────────
276
+
277
+ export const directRelationExpr = zodOp(schemas.directRelationExpr)(
278
+ merge(
279
+ property(
280
+ 'source',
281
+ fqnExpr(),
282
+ ),
283
+ print(v => v.isBidirectional ? ' <-> ' : ' -> '),
284
+ property(
285
+ 'target',
286
+ fqnExpr(),
287
+ ),
288
+ ),
289
+ )
290
+
291
+ export const incomingRelationExpr = zodOp(schemas.incomingRelationExpr)(
292
+ merge(
293
+ print('-> '),
294
+ property(
295
+ 'incoming',
296
+ fqnExpr(),
297
+ ),
298
+ ),
299
+ )
300
+
301
+ export const outgoingRelationExpr = zodOp(schemas.outgoingRelationExpr)(
302
+ merge(
303
+ property(
304
+ 'outgoing',
305
+ fqnExpr(),
306
+ ),
307
+ print(' ->'),
308
+ ),
309
+ )
310
+
311
+ export const inOutRelationExpr = zodOp(schemas.inoutRelationExpr)(
312
+ merge(
313
+ print('-> '),
314
+ property(
315
+ 'inout',
316
+ fqnExpr(),
317
+ ),
318
+ print(' ->'),
319
+ ),
320
+ )
321
+
322
+ export const relationExpr = zodOp(schemas.relationExpr)(({ ctx, exec }) => {
323
+ if ('source' in ctx) {
324
+ return exec(ctx, directRelationExpr())
325
+ }
326
+ if ('incoming' in ctx) {
327
+ return exec(ctx, incomingRelationExpr())
328
+ }
329
+ if ('outgoing' in ctx) {
330
+ return exec(ctx, outgoingRelationExpr())
331
+ }
332
+ if ('inout' in ctx) {
333
+ return exec(ctx, inOutRelationExpr())
334
+ }
335
+ nonexhaustive(ctx)
336
+ })
337
+
338
+ export const relationExprOrWhere = zodOp(schemas.relationExprOrWhere)(({ ctx, out }) => {
339
+ if ('where' in ctx) {
340
+ return merge(
341
+ withctx(ctx.where.expr)(
342
+ relationExpr(),
343
+ ),
344
+ indent(
345
+ print('where'),
346
+ indent(
347
+ withctx(ctx.where.condition)(
348
+ whereOperator(),
349
+ ),
350
+ ),
351
+ ),
352
+ )({ ctx, out })
353
+ }
354
+ return relationExpr()({ ctx, out })
355
+ })
356
+
357
+ export const relationExprCustom = zodOp(schemas.relationExprCustom)(({ ctx: { customRelation }, exec }) => {
358
+ exec(
359
+ customRelation.expr,
360
+ relationExprOrWhere(),
361
+ )
362
+
363
+ const customOp = withctx(customRelation)(
364
+ body('with')(
365
+ titleProperty(),
366
+ descriptionProperty(),
367
+ notationProperty(),
368
+ markdownProperty('notes'),
369
+ property('navigateTo'),
370
+ styleProperties(),
371
+ property('head'),
372
+ property('tail'),
373
+ property('line'),
374
+ ),
375
+ )
376
+ const hasWhere = 'where' in customRelation.expr
377
+
378
+ if (hasWhere) {
379
+ exec({}, indent(customOp))
380
+ } else {
381
+ exec({}, space(), customOp)
382
+ }
383
+ })
384
+
385
+ export const relationExprAny = zodOp(schemas.relationExprAny)(({ ctx, exec }) => {
386
+ if ('customRelation' in ctx) {
387
+ return exec(ctx, relationExprCustom())
388
+ }
389
+ return exec(ctx, relationExprOrWhere())
390
+ })
391
+
392
+ // ──────────────────────────────────────────────
393
+ // Expression dispatcher (expression.ts types)
394
+ // ──────────────────────────────────────────────
395
+
396
+ export const expression = zodOp(schemas.expression)(({ ctx, exec }) => {
397
+ if ('custom' in ctx) {
398
+ return exec(ctx, fqnExprCustom())
399
+ }
400
+ if ('customRelation' in ctx) {
401
+ return exec(ctx, relationExprCustom())
402
+ }
403
+ if ('wildcard' in ctx || 'ref' in ctx || 'elementKind' in ctx || 'elementTag' in ctx) {
404
+ return exec(ctx, fqnExpr())
405
+ }
406
+ if ('source' in ctx || 'incoming' in ctx || 'outgoing' in ctx || 'inout' in ctx) {
407
+ return exec(ctx, relationExpr())
408
+ }
409
+ if ('where' in ctx) {
410
+ const { expr, condition } = ctx.where
411
+ if ('source' in expr || 'incoming' in expr || 'outgoing' in expr || 'inout' in expr) {
412
+ return exec({
413
+ where: {
414
+ expr,
415
+ condition,
416
+ },
417
+ }, relationExprOrWhere())
418
+ }
419
+ return exec({ where: { expr, condition } }, fqnExprOrWhere())
420
+ }
421
+ nonexhaustive(ctx)
422
+ })
@@ -0,0 +1,13 @@
1
+ export * as base from './base'
2
+ export * as deployments from './deployment'
3
+ export { deployment } from './deployment'
4
+ export * as expr from './expressions'
5
+ export { expression } from './expressions'
6
+ export { likec4data } from './likec4data'
7
+ export * as models from './model'
8
+ export { model } from './model'
9
+ export * as props from './properties'
10
+ export * as specifications from './specification'
11
+ export { specification } from './specification'
12
+ export { deploymentView, dynamicView, elementView } from './views'
13
+ export * as views from './views'