@likec4/core 1.17.1 → 1.19.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.
Files changed (184) hide show
  1. package/LICENSE +1 -1
  2. package/dist/builder/index.d.mts +502 -0
  3. package/dist/builder/index.d.ts +502 -0
  4. package/dist/builder/index.js +871 -0
  5. package/dist/compute-view/index.d.mts +22 -0
  6. package/dist/compute-view/index.d.ts +22 -0
  7. package/dist/compute-view/index.js +78 -0
  8. package/dist/index.d.mts +10 -919
  9. package/dist/index.d.ts +10 -919
  10. package/dist/{index.mjs → index.js} +7 -1688
  11. package/dist/model/index.d.mts +254 -0
  12. package/dist/model/index.d.ts +254 -0
  13. package/dist/model/index.js +1 -0
  14. package/dist/shared/chunk-RAAYCPUM.M-JWF7SS.js +21 -0
  15. package/dist/shared/core.BIfgabEw.d.ts +719 -0
  16. package/dist/shared/core.BZ9Msq7w.d.mts +719 -0
  17. package/dist/shared/{core.CoLQkcYL.d.cts → core.C05RDLhi.d.ts} +460 -362
  18. package/dist/shared/{core.CoLQkcYL.d.mts → core.CmYMqgha.d.mts} +460 -362
  19. package/dist/shared/core.D6-fWbM3.d.ts +127 -0
  20. package/dist/shared/core.D74xkKkG.d.mts +148 -0
  21. package/dist/shared/core.D74xkKkG.d.ts +148 -0
  22. package/dist/shared/core.sLaWHBjR.d.mts +127 -0
  23. package/dist/shared/index.-BdzdI2C.js +7443 -0
  24. package/dist/shared/object_hash.CE_oF3I3.js +1222 -0
  25. package/dist/shared/{core.BXTs-8n7.mjs → view.CyZrG8BJ.js} +108 -47
  26. package/dist/types/index.d.mts +71 -1
  27. package/dist/types/index.d.ts +71 -1
  28. package/dist/types/index.js +20 -0
  29. package/dist/utils/index.d.mts +404 -0
  30. package/dist/utils/index.d.ts +404 -0
  31. package/dist/utils/index.js +330 -0
  32. package/package.json +59 -47
  33. package/src/builder/Builder-style1.test-d.ts +190 -0
  34. package/src/builder/Builder-style2.test-d.ts +175 -0
  35. package/src/builder/Builder.deployment.ts +224 -0
  36. package/src/builder/Builder.deploymentModel.ts +385 -0
  37. package/src/builder/Builder.element.ts +19 -19
  38. package/src/builder/Builder.model.ts +67 -42
  39. package/src/builder/Builder.ts +533 -367
  40. package/src/builder/Builder.view-common.ts +234 -0
  41. package/src/builder/Builder.view-deployment.ts +135 -0
  42. package/src/builder/Builder.view-element.ts +189 -0
  43. package/src/builder/Builder.views.ts +109 -33
  44. package/src/builder/Builder.with.ts +475 -0
  45. package/src/builder/__snapshots__/Builder-style1.spec.ts.snap +374 -0
  46. package/src/builder/__snapshots__/Builder-style2.spec.ts.snap +183 -0
  47. package/src/builder/__snapshots__/Builder.spec.ts.snap +374 -0
  48. package/src/builder/_types.ts +113 -18
  49. package/src/builder/index.ts +10 -4
  50. package/src/colors/index.ts +2 -2
  51. package/src/compute-view/compute-view.ts +83 -0
  52. package/src/compute-view/deployment-view/__test__/TestHelper.ts +236 -0
  53. package/src/compute-view/deployment-view/__test__/fixture.ts +198 -0
  54. package/src/compute-view/deployment-view/_types.ts +37 -0
  55. package/src/compute-view/deployment-view/clean-connections.ts +238 -0
  56. package/src/compute-view/deployment-view/compute.ts +125 -0
  57. package/src/compute-view/deployment-view/memory/index.ts +9 -0
  58. package/src/compute-view/deployment-view/memory/memory.ts +97 -0
  59. package/src/compute-view/deployment-view/predicates/elements.ts +144 -0
  60. package/src/compute-view/deployment-view/predicates/relation-direct.ts +237 -0
  61. package/src/compute-view/deployment-view/predicates/relation-in-out.ts +61 -0
  62. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +95 -0
  63. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +88 -0
  64. package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
  65. package/src/compute-view/deployment-view/stages/stage-exclude.ts +5 -0
  66. package/src/compute-view/deployment-view/stages/stage-final.ts +245 -0
  67. package/src/compute-view/deployment-view/stages/stage-include.ts +95 -0
  68. package/src/compute-view/deployment-view/utils.ts +276 -0
  69. package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
  70. package/src/compute-view/dynamic-view/compute.ts +324 -0
  71. package/src/compute-view/element-view/__test__/TestHelper.ts +258 -0
  72. package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +458 -0
  73. package/src/compute-view/element-view/__test__/fixture.ts +675 -0
  74. package/src/compute-view/element-view/_types.ts +48 -0
  75. package/src/compute-view/element-view/clean-connections.ts +113 -0
  76. package/src/compute-view/element-view/compute.ts +315 -0
  77. package/src/compute-view/element-view/memory/NodeGroup.ts +31 -0
  78. package/src/compute-view/element-view/memory/index.ts +10 -0
  79. package/src/compute-view/element-view/memory/memory.ts +177 -0
  80. package/src/compute-view/element-view/memory/stage-exclude.ts +59 -0
  81. package/src/compute-view/element-view/memory/stage-final.ts +129 -0
  82. package/src/compute-view/element-view/memory/stage-include.ts +128 -0
  83. package/src/compute-view/element-view/predicates/_utils.ts +86 -0
  84. package/src/compute-view/element-view/predicates/element-expand.ts +33 -0
  85. package/src/compute-view/element-view/predicates/element-kind-tag.ts +26 -0
  86. package/src/compute-view/element-view/predicates/element-ref.ts +37 -0
  87. package/src/compute-view/element-view/predicates/relation-direct.ts +309 -0
  88. package/src/compute-view/element-view/predicates/relation-in-out.ts +66 -0
  89. package/src/compute-view/element-view/predicates/relation-in.ts +118 -0
  90. package/src/compute-view/element-view/predicates/relation-out.ts +116 -0
  91. package/src/compute-view/element-view/predicates/wildcard.ts +88 -0
  92. package/src/compute-view/element-view/utils.ts +59 -0
  93. package/src/compute-view/index.ts +4 -0
  94. package/src/compute-view/memory/AbstractMemory.ts +45 -0
  95. package/src/compute-view/memory/AbstractStageExclude.ts +216 -0
  96. package/src/compute-view/memory/AbstractStageInclude.ts +200 -0
  97. package/src/compute-view/memory/_types.ts +147 -0
  98. package/src/compute-view/memory/index.ts +5 -0
  99. package/src/compute-view/memory/ops.ts +43 -0
  100. package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
  101. package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
  102. package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
  103. package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
  104. package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
  105. package/src/compute-view/utils/buildComputedNodes.ts +148 -0
  106. package/src/compute-view/utils/buildElementNotations.ts +63 -0
  107. package/src/compute-view/utils/elementExpressionToPredicate.ts +50 -0
  108. package/src/compute-view/utils/link-nodes-with-edges.ts +54 -0
  109. package/src/compute-view/utils/merge-props-from-relationships.ts +128 -0
  110. package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
  111. package/src/compute-view/utils/resolve-extended-views.ts +69 -0
  112. package/src/compute-view/utils/resolve-global-rules.ts +83 -0
  113. package/src/compute-view/utils/topological-sort.ts +256 -0
  114. package/src/compute-view/utils/uniqueTags.test.ts +42 -0
  115. package/src/compute-view/utils/uniqueTags.ts +21 -0
  116. package/src/compute-view/utils/view-hash.ts +27 -0
  117. package/src/compute-view/utils/with-readable-edges.ts +36 -0
  118. package/src/index.ts +22 -12
  119. package/src/model/DeploymentElementModel.ts +599 -0
  120. package/src/model/DeploymentModel.ts +370 -0
  121. package/src/model/ElementModel.ts +221 -0
  122. package/src/model/LikeC4Model.test-d.ts +152 -0
  123. package/src/model/LikeC4Model.ts +336 -457
  124. package/src/model/RelationModel.ts +85 -0
  125. package/src/model/__test__/fixture.ts +149 -338
  126. package/src/model/connection/Connection.ts +72 -0
  127. package/src/model/connection/deployment/DeploymentConnectionModel.ts +178 -0
  128. package/src/model/connection/deployment/find.ts +120 -0
  129. package/src/model/connection/deployment/index.ts +8 -0
  130. package/src/model/connection/index.ts +22 -0
  131. package/src/model/connection/model/ConnectionModel.ts +157 -0
  132. package/src/model/connection/model/find.ts +117 -0
  133. package/src/model/connection/model/index.ts +8 -0
  134. package/src/model/connection/ops.ts +274 -0
  135. package/src/model/guards.ts +9 -0
  136. package/src/model/index.ts +46 -2
  137. package/src/model/types.ts +112 -15
  138. package/src/model/view/EdgeModel.ts +103 -0
  139. package/src/model/view/LikeC4ViewModel.ts +226 -0
  140. package/src/model/view/NodeModel.ts +229 -0
  141. package/src/types/_common.ts +55 -1
  142. package/src/types/deployments.ts +169 -0
  143. package/src/types/element.ts +9 -5
  144. package/src/types/expression-v2.ts +172 -0
  145. package/src/types/expression.ts +17 -11
  146. package/src/types/global.ts +3 -3
  147. package/src/types/index.ts +220 -12
  148. package/src/types/model.ts +75 -12
  149. package/src/types/operators.ts +1 -1
  150. package/src/types/overview-graph.ts +2 -2
  151. package/src/types/relation.ts +18 -14
  152. package/src/types/theme.ts +0 -1
  153. package/src/types/view-notation.ts +2 -2
  154. package/src/types/view.ts +117 -41
  155. package/src/utils/{commonHead.ts → common-head.ts} +2 -0
  156. package/src/utils/const.ts +1 -0
  157. package/src/utils/fqn.ts +203 -56
  158. package/src/utils/getOrCreate.ts +42 -0
  159. package/src/utils/graphlib.ts +10 -0
  160. package/src/utils/index.ts +74 -6
  161. package/src/utils/iterable/_types.ts +5 -0
  162. package/src/utils/iterable/filter.ts +55 -0
  163. package/src/utils/iterable/flat.ts +17 -0
  164. package/src/utils/iterable/index.ts +12 -0
  165. package/src/utils/iterable/map.ts +40 -0
  166. package/src/utils/iterable/reduce.ts +24 -0
  167. package/src/utils/iterable/some.ts +36 -0
  168. package/src/utils/iterable/to.ts +19 -0
  169. package/src/utils/iterable/unique.ts +29 -0
  170. package/src/utils/object-hash.ts +3 -0
  171. package/src/utils/relations.ts +3 -64
  172. package/src/utils/set.ts +78 -0
  173. package/src/utils/string-hash.ts +16 -0
  174. package/dist/index.cjs +0 -5452
  175. package/dist/index.d.cts +0 -1053
  176. package/dist/shared/core.CoLQkcYL.d.ts +0 -734
  177. package/dist/shared/core.DIYZvpVg.cjs +0 -353
  178. package/dist/types/index.cjs +0 -70
  179. package/dist/types/index.d.cts +0 -2
  180. package/dist/types/index.mjs +0 -1
  181. package/src/builder/Builder.test-d.ts +0 -69
  182. package/src/builder/Builder.view.ts +0 -358
  183. package/src/model/LikeC4DiagramModel.ts +0 -338
  184. package/src/model/LikeC4ViewModel.ts +0 -320
@@ -0,0 +1,324 @@
1
+ import { filter, findLast, flatMap, hasAtLeast, isTruthy, map, only, pipe, reduce, unique } from 'remeda'
2
+ import { nonNullable } from '../../errors'
3
+ import type { LikeC4Model } from '../../model'
4
+ import { findConnection } from '../../model/connection/model'
5
+ import type { ElementModel } from '../../model/ElementModel'
6
+ import type { AnyAux } from '../../model/types'
7
+ import {
8
+ type Color,
9
+ type ComputedDynamicView,
10
+ type ComputedEdge,
11
+ DefaultArrowType,
12
+ DefaultLineStyle,
13
+ DefaultRelationshipColor,
14
+ type DynamicView,
15
+ type DynamicViewStep,
16
+ isDynamicViewParallelSteps,
17
+ isViewRuleAutoLayout,
18
+ isViewRulePredicate,
19
+ type NonEmptyArray,
20
+ type RelationId,
21
+ type RelationshipArrowType,
22
+ type RelationshipLineType,
23
+ type StepEdgeId,
24
+ stepEdgeId,
25
+ type Tag,
26
+ type ViewId
27
+ } from '../../types'
28
+ import { ancestorsFqn, commonAncestor, parentFqn } from '../../utils/fqn'
29
+ import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
30
+ import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
31
+ import { buildComputedNodesFromElements } from '../utils/buildComputedNodes'
32
+ import { buildElementNotations } from '../utils/buildElementNotations'
33
+ import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
34
+ import { resolveGlobalRulesInDynamicView } from '../utils/resolve-global-rules'
35
+ import { calcViewLayoutHash } from '../utils/view-hash'
36
+
37
+ type Element = ElementModel
38
+
39
+ namespace DynamicViewCompute {
40
+ export interface Step {
41
+ id: StepEdgeId
42
+ source: Element
43
+ target: Element
44
+ title: string | null
45
+ description?: string
46
+ technology?: string
47
+ color?: Color
48
+ line?: RelationshipLineType
49
+ head?: RelationshipArrowType
50
+ tail?: RelationshipArrowType
51
+ relations: RelationId[]
52
+ isBackward: boolean
53
+ navigateTo?: ViewId
54
+ tags?: NonEmptyArray<Tag>
55
+ }
56
+ }
57
+
58
+ class DynamicViewCompute {
59
+ // Intermediate state
60
+ private explicits = new Set<Element>()
61
+ private steps = [] as DynamicViewCompute.Step[]
62
+
63
+ constructor(
64
+ protected model: LikeC4Model,
65
+ protected view: DynamicView
66
+ ) {}
67
+
68
+ private addStep(
69
+ {
70
+ source: stepSource,
71
+ target: stepTarget,
72
+ title: stepTitle,
73
+ isBackward,
74
+ navigateTo: stepNavigateTo,
75
+ ...step
76
+ }: DynamicViewStep,
77
+ index: number,
78
+ parent?: number
79
+ ) {
80
+ const id = parent ? stepEdgeId(parent, index) : stepEdgeId(index)
81
+ const source = this.model.element(stepSource)
82
+ const target = this.model.element(stepTarget)
83
+
84
+ this.explicits.add(source)
85
+ this.explicits.add(target)
86
+
87
+ const {
88
+ title,
89
+ relations,
90
+ tags,
91
+ navigateTo: derivedNavigateTo,
92
+ head,
93
+ tail,
94
+ color,
95
+ line,
96
+ notation
97
+ } = this.findRelations(source, target)
98
+
99
+ const navigateTo = isTruthy(stepNavigateTo) && stepNavigateTo !== this.view.id ? stepNavigateTo : derivedNavigateTo
100
+
101
+ this.steps.push({
102
+ id,
103
+ ...step,
104
+ source,
105
+ target,
106
+ title: stepTitle ?? title,
107
+ relations: relations ?? [],
108
+ isBackward: isBackward ?? false,
109
+ ...(navigateTo ? { navigateTo } : {}),
110
+ ...(tags ? { tags } : {}),
111
+ ...(head ? { head } : {}),
112
+ ...(tail ? { tail } : {}),
113
+ ...(color ? { color } : {}),
114
+ ...(line ? { line } : {}),
115
+ ...(notation ? { notation } : {})
116
+ })
117
+ }
118
+
119
+ compute(): ComputedDynamicView {
120
+ const {
121
+ docUri: _docUri, // exclude docUri
122
+ rules: _rules, // exclude rules
123
+ steps: viewSteps,
124
+ ...view
125
+ } = this.view
126
+
127
+ let stepNum = 1
128
+ for (const step of viewSteps) {
129
+ if (isDynamicViewParallelSteps(step)) {
130
+ if (step.__parallel.length === 0) {
131
+ continue
132
+ }
133
+ if (step.__parallel.length === 1) {
134
+ this.addStep(step.__parallel[0]!, stepNum)
135
+ } else {
136
+ step.__parallel.forEach((s, i) => this.addStep(s, i + 1, stepNum))
137
+ }
138
+ } else {
139
+ this.addStep(step, stepNum)
140
+ }
141
+ stepNum++
142
+ }
143
+
144
+ const rules = resolveGlobalRulesInDynamicView(_rules, this.model.globals())
145
+
146
+ for (const rule of rules) {
147
+ if (isViewRulePredicate(rule)) {
148
+ for (const expr of rule.include) {
149
+ const satisfies = elementExprToPredicate(expr)
150
+ for (const e of this.model.elements()) {
151
+ if (satisfies(e)) {
152
+ this.explicits.add(e)
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ const elements = [...this.explicits].map(e => e.$element)
160
+ const nodesMap = buildComputedNodesFromElements(elements)
161
+
162
+ const edges = this.steps.map(({ source, target, relations, title, isBackward, ...step }) => {
163
+ const sourceNode = nonNullable(nodesMap.get(source.id), `Source node ${source.id} not found`)
164
+ const targetNode = nonNullable(nodesMap.get(target.id), `Target node ${target.id} not found`)
165
+ const edge: ComputedEdge = {
166
+ parent: commonAncestor(source.id, target.id),
167
+ source: source.id,
168
+ target: target.id,
169
+ label: title,
170
+ relations,
171
+ color: DefaultRelationshipColor,
172
+ line: DefaultLineStyle,
173
+ head: DefaultArrowType,
174
+ ...step
175
+ }
176
+ if (isBackward) {
177
+ edge.dir = 'back'
178
+ }
179
+
180
+ while (edge.parent && !nodesMap.has(edge.parent)) {
181
+ edge.parent = parentFqn(edge.parent)
182
+ }
183
+ sourceNode.outEdges.push(edge.id)
184
+ targetNode.inEdges.push(edge.id)
185
+ // Process edge source ancestors
186
+ for (const sourceAncestor of ancestorsFqn(edge.source)) {
187
+ if (sourceAncestor === edge.parent) {
188
+ break
189
+ }
190
+ nodesMap.get(sourceAncestor)?.outEdges.push(edge.id)
191
+ }
192
+ // Process target hierarchy
193
+ for (const targetAncestor of ancestorsFqn(edge.target)) {
194
+ if (targetAncestor === edge.parent) {
195
+ break
196
+ }
197
+ nodesMap.get(targetAncestor)?.inEdges.push(edge.id)
198
+ }
199
+ return edge
200
+ })
201
+
202
+ const nodes = applyCustomElementProperties(
203
+ rules,
204
+ applyViewRuleStyles(
205
+ rules,
206
+ // Keep order of elements
207
+ elements.map(e => nonNullable(nodesMap.get(e.id)))
208
+ )
209
+ )
210
+
211
+ const autoLayoutRule = findLast(rules, isViewRuleAutoLayout)
212
+
213
+ const elementNotations = buildElementNotations(nodes)
214
+
215
+ return calcViewLayoutHash({
216
+ ...view,
217
+ autoLayout: {
218
+ direction: autoLayoutRule?.direction ?? 'LR',
219
+ ...(autoLayoutRule?.nodeSep && { nodeSep: autoLayoutRule.nodeSep }),
220
+ ...(autoLayoutRule?.rankSep && { rankSep: autoLayoutRule.rankSep })
221
+ },
222
+ nodes: map(nodes, n => {
223
+ // omit notation
224
+ delete n.notation
225
+ if (n.icon === 'none') {
226
+ delete n.icon
227
+ }
228
+ return n
229
+ }),
230
+ edges,
231
+ ...(elementNotations.length > 0 && {
232
+ notation: {
233
+ elements: elementNotations
234
+ }
235
+ })
236
+ })
237
+ }
238
+
239
+ private findRelations(source: Element, target: Element): {
240
+ title: string | null
241
+ tags: NonEmptyArray<Tag> | null
242
+ relations: NonEmptyArray<RelationId> | null
243
+ navigateTo: ViewId | null
244
+ tail: RelationshipArrowType | null
245
+ head: RelationshipArrowType | null
246
+ color: Color | null
247
+ line: RelationshipLineType | null
248
+ notation: string | null
249
+ } {
250
+ const relationships = findConnection(source, target, 'directed').flatMap(r => [...r.relations])
251
+ if (relationships.length === 0) {
252
+ return {
253
+ title: null,
254
+ tags: null,
255
+ relations: null,
256
+ navigateTo: null,
257
+ tail: null,
258
+ head: null,
259
+ color: null,
260
+ line: null,
261
+ notation: null
262
+ }
263
+ }
264
+ const alltags = pipe(
265
+ relationships,
266
+ flatMap(r => r.tags),
267
+ filter(isTruthy),
268
+ unique()
269
+ )
270
+ const tags = hasAtLeast(alltags, 1) ? alltags : null
271
+ const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
272
+
273
+ // Most closest relation
274
+ const relation = only(relationships) || relationships.find(r => r.source === source && r.target === target)
275
+ const relationNavigateTo = relation?.$relationship.navigateTo ?? null
276
+
277
+ const navigateTo = relationNavigateTo && relationNavigateTo !== this.view.id ? relationNavigateTo : pipe(
278
+ relationships,
279
+ map(r => r.$relationship.navigateTo),
280
+ filter(isTruthy),
281
+ filter(v => v !== this.view.id),
282
+ unique(),
283
+ only()
284
+ )
285
+
286
+ const commonProperties = pipe(
287
+ relationships,
288
+ reduce((acc, { title, $relationship: r }) => {
289
+ isTruthy(title) && acc.title.add(title)
290
+ isTruthy(r.tail) && acc.tail.add(r.tail)
291
+ isTruthy(r.head) && acc.head.add(r.head)
292
+ isTruthy(r.color) && acc.color.add(r.color)
293
+ isTruthy(r.line) && acc.line.add(r.line)
294
+
295
+ return acc
296
+ }, {
297
+ tail: new Set<RelationshipArrowType>(),
298
+ head: new Set<RelationshipArrowType>(),
299
+ color: new Set<Color>(),
300
+ line: new Set<RelationshipLineType>(),
301
+ notation: new Set<string>(),
302
+ title: new Set<string>()
303
+ })
304
+ )
305
+
306
+ return {
307
+ tags,
308
+ relations,
309
+ navigateTo: navigateTo ?? null,
310
+ title: only([...commonProperties.title]) ?? null,
311
+ tail: only([...commonProperties.tail]) ?? null,
312
+ head: only([...commonProperties.head]) ?? null,
313
+ color: only([...commonProperties.color]) ?? null,
314
+ line: only([...commonProperties.line]) ?? null,
315
+ notation: only([...commonProperties.notation]) ?? null
316
+ }
317
+ }
318
+ }
319
+ export function computeDynamicView<M extends AnyAux>(
320
+ model: LikeC4Model<M>,
321
+ view: DynamicView
322
+ ): ComputedDynamicView {
323
+ return new DynamicViewCompute(model, view).compute()
324
+ }
@@ -0,0 +1,258 @@
1
+ import { indexBy, isArray, isString, map, mapValues, pipe, prop } from 'remeda'
2
+ import type { Writable } from 'type-fest'
3
+ import type { ExpectStatic } from 'vitest'
4
+ import { expect as vitestExpect } from 'vitest'
5
+ import {
6
+ type AnyTypes,
7
+ type Builder,
8
+ type ElementViewRulesBuilder,
9
+ type Types,
10
+ type ViewPredicate,
11
+ } from '../../../builder'
12
+ import * as viewhelpers from '../../../builder/Builder.view-common'
13
+ import { mkViewBuilder } from '../../../builder/Builder.views'
14
+ import { differenceConnections } from '../../../model/connection'
15
+ import type { ComputedElementView, ElementView, ViewId, ViewRule } from '../../../types'
16
+ import { compareNatural } from '../../../utils'
17
+ import { imap, toArray } from '../../../utils/iterable'
18
+ import { difference as differenceSet } from '../../../utils/set'
19
+ import type { CtxConnection, CtxElement } from '../../memory'
20
+ import { withReadableEdges } from '../../utils/with-readable-edges'
21
+ import { processPredicates as processPredicatesImpl } from '../compute'
22
+ import { type Ctx, Memory } from '../memory'
23
+
24
+ type ConnectionExpression<T extends AnyTypes> = `${T['Fqn']} -> ${T['Fqn']}`
25
+
26
+ // type ConnectionsDeepMatcher<T extends AnyTypes> = {
27
+ // [K in `${T['Fqn']} -> ${T['Fqn']}`]?: {
28
+ // relations?: Array<`${T['Fqn']} -> ${T['Fqn']}`>
29
+ // }
30
+ // }
31
+
32
+ type ConnectionsDeepMatcher<Expr extends string> = {
33
+ [K in Expr]?: Array<Expr>
34
+ }
35
+
36
+ type ConnectionEqual<T extends AnyTypes> = [ConnectionsDeepMatcher<ConnectionExpression<T>>] | [
37
+ ConnectionExpression<T>,
38
+ ...ConnectionExpression<T>[],
39
+ ]
40
+
41
+ type Elem = CtxElement<Ctx>
42
+ type Connection = CtxConnection<Ctx>
43
+
44
+ export class TestHelper<T extends AnyTypes> {
45
+ model: Types.ToLikeC4Model<T>
46
+
47
+ static $include = viewhelpers.$include
48
+ static $exclude = viewhelpers.$exclude
49
+ static $rules = viewhelpers.$rules
50
+ static $style = viewhelpers.$style
51
+
52
+ $include = viewhelpers.$include
53
+ $exclude = viewhelpers.$exclude
54
+ $style = viewhelpers.$style
55
+
56
+ static from<const T extends AnyTypes>(builder: Builder<T>, expect = vitestExpect): TestHelper<T> {
57
+ return new TestHelper(builder, expect)
58
+ }
59
+
60
+ constructor(
61
+ private builder: Builder<T>,
62
+ private _expect: ExpectStatic,
63
+ ) {
64
+ this.model = builder.toLikeC4Model() as Types.ToLikeC4Model<T>
65
+ }
66
+
67
+ computeView = (...rules: ElementViewRulesBuilder<T>[]) => {
68
+ return withReadableEdges(
69
+ this.builder
70
+ .clone()
71
+ .views(_ => _.view('dev').with(...rules))
72
+ .toLikeC4Model()
73
+ .view('dev')
74
+ .$view as ComputedElementView<'dev'>,
75
+ ' -> ',
76
+ )
77
+ }
78
+
79
+ processPredicates(...rules: ElementViewRulesBuilder<T>[]) {
80
+ return ProcessPredicates.execute(this, ...rules)
81
+ }
82
+ processPredicatesWithScope(scope: T['Fqn'], ...rules: ElementViewRulesBuilder<T>[]) {
83
+ return ProcessPredicates.executeWithScope(this, scope, ...rules)
84
+ }
85
+
86
+ expectView(view: ComputedElementView) {
87
+ return {
88
+ toHave: (nodesAndEdges: { nodes: Array<T['Fqn']>; edges: Array<ConnectionExpression<T>> }) => {
89
+ const actual = {
90
+ nodes: view.nodes.map(prop('id')),
91
+ edges: view.edges.map(prop('id')),
92
+ }
93
+ this._expect(actual).toEqual(nodesAndEdges)
94
+ },
95
+ toHaveNodes: <Id extends T['Fqn']>(...nodes: Id[]) => {
96
+ this._expect(view.nodes.map(prop('id'))).toEqual(nodes)
97
+ },
98
+ toHaveEdges: <Id extends ConnectionExpression<T>>(...edges: Id[]) => {
99
+ this._expect(view.edges.map(prop('id'))).toEqual(edges)
100
+ },
101
+ }
102
+ }
103
+
104
+ expectComputedView(...rules: ElementViewRulesBuilder<T>[]) {
105
+ return this.expectView(this.computeView(...rules))
106
+ }
107
+
108
+ expectMemory = (memory: Memory) => ({
109
+ toHaveAllElements: <Id extends T['Fqn']>(...ids: Id[]) => {
110
+ this._expect(map([...memory.elements], prop('id'))).toEqual(ids)
111
+ },
112
+ /**
113
+ * Final elements (visible in the view)
114
+ */
115
+ toHaveElements: <Id extends T['Fqn']>(...ids: Id[]) => {
116
+ this._expect(map([...memory.final], prop('id'))).toEqual(ids)
117
+ },
118
+ toHaveConnections: (...matchers: ConnectionEqual<T>) => {
119
+ this.expectConnections(memory.connections).toEqual(...matchers)
120
+ },
121
+ })
122
+
123
+ expectElements = (elements: ReadonlySet<Elem>) => ({
124
+ toEqual: <const Id extends ViewPredicate.DeploymentConnectionExpression<T>>(...ids: Id[]) => {
125
+ this._expect(toArray(imap(elements, prop('id')))).toEqual(ids)
126
+ },
127
+ })
128
+
129
+ expectConnections = (connections: ReadonlyArray<Connection>) => ({
130
+ toBeEmpty: () => {
131
+ this._expect(connections.map(c => c.expression)).to.be.empty
132
+ },
133
+ toEqual: (...matchers: ConnectionEqual<T>) => {
134
+ const [matcher, ...rest] = matchers
135
+ if (isString(matcher)) {
136
+ this._expect(connections.map(c => c.expression)).toEqual([matcher, ...rest])
137
+ return
138
+ }
139
+ const obj = pipe(
140
+ connections,
141
+ indexBy(prop('expression')),
142
+ mapValues(c => [...c.relations].map(prop('expression')).sort(compareNatural)),
143
+ )
144
+ const expected = mapValues(matcher, (v: any) => [...v].sort(compareNatural))
145
+ this._expect(obj).toEqual(expected)
146
+ },
147
+ })
148
+
149
+ expectStep = (step: ProcessPredicates<T>) => ({
150
+ ...this.expectMemory(step.memory),
151
+ })
152
+
153
+ expect(value: ComputedElementView): ReturnType<typeof this['expectView']>
154
+ expect(value: Set<Elem>): ReturnType<typeof this['expectElements']>
155
+ expect(value: ReadonlyArray<Connection>): ReturnType<typeof this['expectConnections']>
156
+ expect(value: Memory | ProcessPredicates<T>): ReturnType<typeof this['expectMemory']>
157
+ expect(value: Memory | ProcessPredicates<T> | Set<Elem> | ComputedElementView | ReadonlyArray<Connection>) {
158
+ if (value instanceof Memory) {
159
+ return this.expectMemory(value)
160
+ }
161
+ if (value instanceof ProcessPredicates) {
162
+ return this.expectStep(value)
163
+ }
164
+ if (value instanceof Set) {
165
+ return this.expectElements(value)
166
+ }
167
+ if (isArray(value)) {
168
+ return this.expectConnections(value)
169
+ }
170
+ return this.expectView(value)
171
+ }
172
+ }
173
+
174
+ class ProcessPredicates<T extends AnyTypes> {
175
+ static execute<A extends AnyTypes>(
176
+ test: TestHelper<A>,
177
+ ...rules: ElementViewRulesBuilder<A>[]
178
+ ): ProcessPredicates<A> {
179
+ const processor = new ProcessPredicates(test)
180
+ processor.next(...rules)
181
+ return processor
182
+ }
183
+
184
+ static executeWithScope<A extends AnyTypes>(
185
+ test: TestHelper<A>,
186
+ scope: A['Fqn'],
187
+ ...rules: ElementViewRulesBuilder<A>[]
188
+ ): ProcessPredicates<A> {
189
+ const processor = new ProcessPredicates(test, scope)
190
+ processor.next(...rules)
191
+ return processor
192
+ }
193
+
194
+ public viewrules: ReadonlyArray<ViewRule> = []
195
+
196
+ public previousMemory: Memory = Memory.empty(null)
197
+ public memory: Memory = Memory.empty(null)
198
+ public predicates: ElementViewRulesBuilder<T>[] = []
199
+
200
+ constructor(
201
+ public readonly t: TestHelper<T>,
202
+ public scope: T['Fqn'] | null = null,
203
+ protected age = 0,
204
+ ) {}
205
+
206
+ get elements() {
207
+ return toArray(imap(this.memory.elements, prop('id')))
208
+ }
209
+
210
+ get connections() {
211
+ return this.memory.connections.map(c => c.expression)
212
+ }
213
+
214
+ diff() {
215
+ const prevState = this.previousMemory
216
+ const state = this.memory
217
+ return {
218
+ added: this.memory.update({
219
+ elements: differenceSet(state.elements, prevState.elements),
220
+ explicits: differenceSet(state.explicits, prevState.explicits),
221
+ final: differenceSet(state.final, prevState.final),
222
+ connections: differenceConnections(state.connections, prevState.connections),
223
+ }),
224
+ removed: this.memory.update({
225
+ elements: differenceSet(prevState.elements, state.elements),
226
+ explicits: differenceSet(prevState.explicits, state.explicits),
227
+ final: differenceSet(prevState.final, state.final),
228
+ connections: differenceConnections(prevState.connections, state.connections),
229
+ }),
230
+ }
231
+ }
232
+
233
+ next(...predicates: ElementViewRulesBuilder<T>[]): this {
234
+ const view = {
235
+ id: 'test' as ViewId,
236
+ __: 'element',
237
+ rules: [],
238
+ } as any as Writable<ElementView>
239
+ let vb = mkViewBuilder(view) as any
240
+ this.predicates = [
241
+ ...this.predicates,
242
+ ...predicates,
243
+ ]
244
+ for (const rule of this.predicates) {
245
+ rule(vb)
246
+ }
247
+ this.previousMemory = this.memory
248
+ this.viewrules = view.rules
249
+ const scope = this.scope ? this.t.model.element(this.scope) : null
250
+ this.memory = processPredicatesImpl(
251
+ this.t.model,
252
+ Memory.empty(scope),
253
+ view.rules,
254
+ )
255
+ this.age++
256
+ return this
257
+ }
258
+ }