@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.
- package/dist/_chunks/chunk.mjs +11 -0
- package/dist/index.d.mts +18 -1
- package/dist/index.mjs +189 -47
- package/dist/likec4/index.d.mts +277169 -0
- package/dist/likec4/index.mjs +1799 -0
- package/likec4/package.json +4 -0
- package/package.json +22 -8
- package/src/drawio/generate-drawio.ts +73 -8
- package/src/drawio/index.ts +1 -0
- package/src/drawio/parse-drawio.ts +172 -18
- package/src/index.ts +2 -0
- package/src/likec4/generate-likec4.ts +72 -0
- package/src/likec4/index.ts +12 -0
- package/src/likec4/operators/base.ts +938 -0
- package/src/likec4/operators/deployment.ts +263 -0
- package/src/likec4/operators/expressions.ts +422 -0
- package/src/likec4/operators/index.ts +13 -0
- package/src/likec4/operators/likec4data.ts +33 -0
- package/src/likec4/operators/model.ts +222 -0
- package/src/likec4/operators/properties.ts +244 -0
- package/src/likec4/operators/specification.ts +119 -0
- package/src/likec4/operators/views.ts +390 -0
- package/src/likec4/schemas/common.ts +123 -0
- package/src/likec4/schemas/deployment.ts +113 -0
- package/src/likec4/schemas/expression.ts +218 -0
- package/src/likec4/schemas/index.ts +83 -0
- package/src/likec4/schemas/likec4data.ts +76 -0
- package/src/likec4/schemas/model.ts +127 -0
- package/src/likec4/schemas/specification.ts +83 -0
- package/src/likec4/schemas/views.ts +321 -0
- package/src/model/generate-likec4.ts +0 -5
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { type MarkdownOrString, hasProp, invariant } from '@likec4/core'
|
|
2
|
+
import {
|
|
3
|
+
type Generated,
|
|
4
|
+
type GeneratorNode,
|
|
5
|
+
type JoinOptions,
|
|
6
|
+
CompositeGeneratorNode,
|
|
7
|
+
joinToNode,
|
|
8
|
+
NewLineNode,
|
|
9
|
+
NL,
|
|
10
|
+
toString,
|
|
11
|
+
} from 'langium/generate'
|
|
12
|
+
import {
|
|
13
|
+
filter,
|
|
14
|
+
hasAtLeast,
|
|
15
|
+
identity,
|
|
16
|
+
isArray,
|
|
17
|
+
isFunction,
|
|
18
|
+
isNonNullish,
|
|
19
|
+
isNullish,
|
|
20
|
+
isNumber,
|
|
21
|
+
isObjectType,
|
|
22
|
+
isString,
|
|
23
|
+
map,
|
|
24
|
+
only,
|
|
25
|
+
pipe,
|
|
26
|
+
} from 'remeda'
|
|
27
|
+
import { dedent } from 'strip-indent'
|
|
28
|
+
import type { IfAny, Or } from 'type-fest'
|
|
29
|
+
import * as z from 'zod/v4'
|
|
30
|
+
import type * as z4 from 'zod/v4/core'
|
|
31
|
+
|
|
32
|
+
function hasContent(out: Generated): boolean {
|
|
33
|
+
if (typeof out === 'string') {
|
|
34
|
+
return out.trimStart().length !== 0
|
|
35
|
+
}
|
|
36
|
+
if (out instanceof CompositeGeneratorNode) {
|
|
37
|
+
return out.contents.some(e => hasContent(e))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasContentOrNewLine(out: Generated): boolean {
|
|
44
|
+
if (typeof out === 'string') {
|
|
45
|
+
return out.trimStart().length !== 0
|
|
46
|
+
}
|
|
47
|
+
if (out instanceof NewLineNode) {
|
|
48
|
+
return !out.ifNotEmpty
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (out instanceof CompositeGeneratorNode) {
|
|
52
|
+
return out.contents.some(e => hasContentOrNewLine(e))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Output = CompositeGeneratorNode
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Context for operation, contains the value and target output node
|
|
62
|
+
*
|
|
63
|
+
* @typeParam A - Context value
|
|
64
|
+
* @see executeOnCtx
|
|
65
|
+
*/
|
|
66
|
+
export type Ctx<A> = {
|
|
67
|
+
ctx: A
|
|
68
|
+
out: Output
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type AnyCtx = Ctx<any>
|
|
72
|
+
|
|
73
|
+
export interface Op<A> {
|
|
74
|
+
(value: Ctx<A>): Ctx<A>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CtxOp<A> {
|
|
78
|
+
(value: A): A
|
|
79
|
+
}
|
|
80
|
+
export type AnyOp = Op<any>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Infer the context type from an operation
|
|
84
|
+
*/
|
|
85
|
+
export type InferOp = <A>(ctx: A) => A
|
|
86
|
+
|
|
87
|
+
export type ctxOf<O> =
|
|
88
|
+
// dprint-ignore
|
|
89
|
+
O extends Op<infer A>
|
|
90
|
+
? A
|
|
91
|
+
: O extends (...args: any[]) => Op<infer B>
|
|
92
|
+
? B
|
|
93
|
+
: never
|
|
94
|
+
|
|
95
|
+
export type Ops<A> = Op<A>[]
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a new context with the given value and an empty output node
|
|
99
|
+
* @see executeOnFresh
|
|
100
|
+
*/
|
|
101
|
+
export function fresh(): Ctx<never>
|
|
102
|
+
export function fresh<A>(ctx: A): IfAny<A, never, Ctx<A>>
|
|
103
|
+
export function fresh(ctx?: unknown) {
|
|
104
|
+
return { ctx: ctx ?? undefined, out: new CompositeGeneratorNode() } as never
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Materialize context or operation into a string
|
|
109
|
+
*/
|
|
110
|
+
export function materialize(ctx: AnyCtx | Op<any>, defaultIndentation: string | number = 2): string {
|
|
111
|
+
const out = isFunction(ctx) ? executeOnFresh(undefined, [ctx]).out : ctx.out
|
|
112
|
+
return toString(out, defaultIndentation).replaceAll(/\r\n/g, '\n')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute a sequence of operations on the given context
|
|
117
|
+
* This is reduce operation
|
|
118
|
+
* @see withctx
|
|
119
|
+
*/
|
|
120
|
+
export function executeOnCtx<A>(
|
|
121
|
+
ctx: Ctx<A>,
|
|
122
|
+
op: Ops<A>,
|
|
123
|
+
): Ctx<A>
|
|
124
|
+
export function executeOnCtx<A>(
|
|
125
|
+
ctx: Ctx<A>,
|
|
126
|
+
op: Op<A>,
|
|
127
|
+
...ops: Ops<A>
|
|
128
|
+
): Ctx<A>
|
|
129
|
+
export function executeOnCtx<A>(
|
|
130
|
+
ctx: Ctx<A>,
|
|
131
|
+
op: Op<A> | Ops<A>,
|
|
132
|
+
...ops: Ops<A>
|
|
133
|
+
): Ctx<A> {
|
|
134
|
+
if (isArray(op)) {
|
|
135
|
+
invariant(ops.length === 0, 'When first argument is an array, no additional operations are allowed')
|
|
136
|
+
ops = op
|
|
137
|
+
} else {
|
|
138
|
+
ops = [op, ...ops]
|
|
139
|
+
}
|
|
140
|
+
for (const o of ops) {
|
|
141
|
+
ctx = o(ctx)
|
|
142
|
+
}
|
|
143
|
+
return ctx
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute a sequence of operations on a fresh context (new empty output node)
|
|
148
|
+
* This is reduce operation
|
|
149
|
+
*/
|
|
150
|
+
export function executeOnFresh<A>(
|
|
151
|
+
ctx: A,
|
|
152
|
+
op: Ops<A>,
|
|
153
|
+
): Ctx<A>
|
|
154
|
+
export function executeOnFresh<A>(
|
|
155
|
+
ctx: A,
|
|
156
|
+
op: Op<A>,
|
|
157
|
+
...ops: Ops<A>
|
|
158
|
+
): Ctx<A>
|
|
159
|
+
export function executeOnFresh<A>(
|
|
160
|
+
ctx: A,
|
|
161
|
+
op: Ops<A> | Op<A>,
|
|
162
|
+
...ops: Ops<A>
|
|
163
|
+
) {
|
|
164
|
+
if (isArray(op)) {
|
|
165
|
+
return executeOnCtx(fresh(ctx), op)
|
|
166
|
+
}
|
|
167
|
+
return executeOnCtx(fresh(ctx), [op, ...ops])
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Execute each operation on a fresh context (new empty output node)
|
|
172
|
+
* This is map operation
|
|
173
|
+
*/
|
|
174
|
+
export function eachOnFresh<A>(
|
|
175
|
+
ctx: A,
|
|
176
|
+
ops: Ops<A>,
|
|
177
|
+
): Ctx<A>[] {
|
|
178
|
+
return ops.map(op => op(fresh(ctx)))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create an operation from a function
|
|
183
|
+
* {@link Op} requires return type to be Ctx<A>, this helper does it for us
|
|
184
|
+
*/
|
|
185
|
+
export function operation<A>(fn: (input: Ctx<A>) => any): Op<A>
|
|
186
|
+
export function operation<A>(name: string, fn: (input: Ctx<A>) => any): Op<A>
|
|
187
|
+
export function operation(opOrName: string | Function, fn?: Function) {
|
|
188
|
+
const operationFn = typeof opOrName === 'function' ? opOrName : fn!
|
|
189
|
+
const wrapped = (input: Ctx<any>) => {
|
|
190
|
+
const result = operationFn(input)
|
|
191
|
+
if (result instanceof CompositeGeneratorNode) {
|
|
192
|
+
return { ...input, out: result }
|
|
193
|
+
}
|
|
194
|
+
return input
|
|
195
|
+
}
|
|
196
|
+
if (typeof opOrName === 'string' && operationFn.name == '') {
|
|
197
|
+
Object.defineProperties(wrapped, {
|
|
198
|
+
name: { value: `wrapped(${opOrName})` },
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
return wrapped
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isPrintable(value: unknown): value is string | number | boolean {
|
|
205
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Prints current context or given parameter.
|
|
209
|
+
* If function is given, it will be called with the current context and the result will be printed (i.e formatted).
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* withctx('value')(
|
|
214
|
+
* print('const and '),
|
|
215
|
+
* print(),
|
|
216
|
+
* )
|
|
217
|
+
* // Output: const and value
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* * @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* withctx({tag: 'one'})(
|
|
223
|
+
* print(c => '#' + c.tag)
|
|
224
|
+
* )
|
|
225
|
+
* // Output: #one
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export function print<A extends string | number | boolean>(): Op<A>
|
|
229
|
+
export function print<A>(format: (value: A) => string): Op<A>
|
|
230
|
+
export function print(value: string | number | boolean): InferOp
|
|
231
|
+
export function print(value?: unknown) {
|
|
232
|
+
return operation(function printOp({ ctx, out }) {
|
|
233
|
+
let v = typeof value === 'function' ? value(ctx) : (value ?? ctx)
|
|
234
|
+
if (isNullish(v) || v === '') {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
invariant(isPrintable(v), 'Value must be a string, number or boolean - got ' + typeof v)
|
|
238
|
+
out.append(String(v))
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const eq = (): InferOp => print('=')
|
|
243
|
+
|
|
244
|
+
export const space = (): InferOp => print(' ')
|
|
245
|
+
|
|
246
|
+
export function noop(): <A>(input: A) => A {
|
|
247
|
+
return identity()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* To be used for recursive operations
|
|
252
|
+
*/
|
|
253
|
+
export function lazy<A>(op: () => Op<A>): Op<A> {
|
|
254
|
+
return (input: Ctx<A>) => {
|
|
255
|
+
op()(input)
|
|
256
|
+
return input
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function newline(when?: 'ifNotEmpty'): InferOp {
|
|
261
|
+
return operation(({ out }) => {
|
|
262
|
+
if (when === 'ifNotEmpty') {
|
|
263
|
+
out.appendNewLineIfNotEmpty()
|
|
264
|
+
} else {
|
|
265
|
+
out.appendNewLine()
|
|
266
|
+
}
|
|
267
|
+
}) as any
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Merge multiple operations into a single output node
|
|
272
|
+
*/
|
|
273
|
+
export function merge<A>(...ops: Ops<A>): Op<A> {
|
|
274
|
+
return operation(function merge({ ctx, out }) {
|
|
275
|
+
const nested = executeOnFresh(ctx as A, ops)
|
|
276
|
+
out.appendIf(
|
|
277
|
+
hasContent(nested.out),
|
|
278
|
+
nested.out,
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Indent output of operations
|
|
285
|
+
*/
|
|
286
|
+
export function indent(value: string): InferOp
|
|
287
|
+
export function indent<A>(...args: Ops<A>): Op<A>
|
|
288
|
+
export function indent(...args: AnyOp[] | [string]) {
|
|
289
|
+
if (args.length === 1 && typeof args[0] === 'string') {
|
|
290
|
+
const text = dedent(args[0] as string)
|
|
291
|
+
if (text.trimStart().length === 0) {
|
|
292
|
+
return noop()
|
|
293
|
+
}
|
|
294
|
+
return operation(function indent1({ out }) {
|
|
295
|
+
out
|
|
296
|
+
.appendNewLineIfNotEmpty()
|
|
297
|
+
.indent({
|
|
298
|
+
indentEmptyLines: true,
|
|
299
|
+
indentedChildren: [
|
|
300
|
+
joinToNode(
|
|
301
|
+
text.split(/\r?\n/),
|
|
302
|
+
{ separator: NL },
|
|
303
|
+
),
|
|
304
|
+
],
|
|
305
|
+
})
|
|
306
|
+
.appendNewLineIfNotEmpty()
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
const ops = args as Ops<unknown>
|
|
310
|
+
return operation(function indent2({ ctx, out }) {
|
|
311
|
+
const nested = executeOnFresh(ctx, ops)
|
|
312
|
+
if (hasContent(nested.out)) {
|
|
313
|
+
out
|
|
314
|
+
.appendNewLineIfNotEmpty()
|
|
315
|
+
.indent({
|
|
316
|
+
indentEmptyLines: true,
|
|
317
|
+
indentedChildren: nested.out.contents,
|
|
318
|
+
})
|
|
319
|
+
.appendNewLineIfNotEmpty()
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Indent output of operations, join them with newlines and wrap them
|
|
326
|
+
* with `open` and `close` params (`{ .. }` by default)
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* body(
|
|
331
|
+
* print('name: John'),
|
|
332
|
+
* print('age: 30'),
|
|
333
|
+
* )
|
|
334
|
+
* // Output:
|
|
335
|
+
* // {
|
|
336
|
+
* // name: John
|
|
337
|
+
* // age: 30
|
|
338
|
+
* // }
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* body('style')(
|
|
344
|
+
* print('color: red'),
|
|
345
|
+
* print('font-size: 12px'),
|
|
346
|
+
* )
|
|
347
|
+
* // Output:
|
|
348
|
+
* // style {
|
|
349
|
+
* // color: red
|
|
350
|
+
* // font-size: 12px
|
|
351
|
+
* // }
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* body('when [',']')(
|
|
357
|
+
* print('some condition'),
|
|
358
|
+
* )
|
|
359
|
+
* // Output:
|
|
360
|
+
* // when [
|
|
361
|
+
* // some condition
|
|
362
|
+
* // ]
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export function body<A>(...ops: Ops<A>): Op<A>
|
|
366
|
+
export function body(keyword: string): <A>(...ops: Ops<A>) => Op<A>
|
|
367
|
+
export function body(open: string, close: string): <A>(...ops: Ops<A>) => Op<A>
|
|
368
|
+
export function body(...args: unknown[]) {
|
|
369
|
+
const keyword = only(args)
|
|
370
|
+
if (isString(keyword)) {
|
|
371
|
+
return body(keyword + ' {', '}')
|
|
372
|
+
}
|
|
373
|
+
if (args.length === 2 && isString(args[0]) && isString(args[1])) {
|
|
374
|
+
const [open, close] = args as [string, string]
|
|
375
|
+
return (...ops: Ops<any>) =>
|
|
376
|
+
operation(function body({ ctx, out }) {
|
|
377
|
+
const bodyOutput = indent(lines(...ops))(fresh(ctx)).out
|
|
378
|
+
out.appendIf(
|
|
379
|
+
hasContent(bodyOutput),
|
|
380
|
+
joinToNode([
|
|
381
|
+
open,
|
|
382
|
+
bodyOutput,
|
|
383
|
+
close,
|
|
384
|
+
]),
|
|
385
|
+
)
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
const ops = args as Ops<any>
|
|
389
|
+
return body('{', '}')(...ops)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const QUOTE = '\''
|
|
393
|
+
const ESCAPED_QUOTE = '\\' + QUOTE
|
|
394
|
+
/**
|
|
395
|
+
* Append text to the current line, wrapped in single quotes
|
|
396
|
+
* Multiple lines are joined with spaces
|
|
397
|
+
* Escapes single quotes by doubling them
|
|
398
|
+
*/
|
|
399
|
+
export function inlineText<A extends string>(): Op<A>
|
|
400
|
+
export function inlineText<A>(value: string): Op<A>
|
|
401
|
+
export function inlineText(value?: string) {
|
|
402
|
+
return operation(({ ctx, out }) => {
|
|
403
|
+
let v = value ?? ctx
|
|
404
|
+
if (isNullish(v)) {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
invariant(isString(v), 'Value must be a string - got ' + typeof v)
|
|
408
|
+
const escapedValue = v
|
|
409
|
+
.replace(/(\r?\n|\t)+/g, ' ')
|
|
410
|
+
.replaceAll(QUOTE, ESCAPED_QUOTE)
|
|
411
|
+
.trim()
|
|
412
|
+
out.append(`${QUOTE}${escapedValue}${QUOTE}`)
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function multilineText(value: string, quotes = QUOTE): AnyOp {
|
|
417
|
+
return merge(
|
|
418
|
+
print(quotes),
|
|
419
|
+
indent(
|
|
420
|
+
value.replaceAll(QUOTE, ESCAPED_QUOTE),
|
|
421
|
+
),
|
|
422
|
+
print(quotes),
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Appends text (handles newlines)
|
|
428
|
+
* If the text contains newlines, it is wrapped in double quotes
|
|
429
|
+
*
|
|
430
|
+
* Escapes single quotes by doubling them
|
|
431
|
+
*/
|
|
432
|
+
export function text<A extends string>(): Op<A>
|
|
433
|
+
export function text<A>(format: (value: A) => string): Op<A>
|
|
434
|
+
export function text(value: string): InferOp
|
|
435
|
+
export function text(value?: unknown) {
|
|
436
|
+
return operation(function text({ ctx, out }) {
|
|
437
|
+
let v = typeof value === 'function' ? value(ctx) : (value ?? ctx)
|
|
438
|
+
if (isNullish(v)) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
invariant(isString(v), 'Value must be a string - got ' + typeof v)
|
|
442
|
+
if (v.includes('\n')) {
|
|
443
|
+
return multilineText(v)({ ctx: v, out })
|
|
444
|
+
}
|
|
445
|
+
return inlineText()({ ctx: v, out })
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const TRIPLE_QUOTE = QUOTE.repeat(3)
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Wraps text in triple quotes (to be transformed to markdown)
|
|
453
|
+
*/
|
|
454
|
+
export function markdown<A extends string>(): Op<A>
|
|
455
|
+
export function markdown<A>(value: string): Op<A>
|
|
456
|
+
export function markdown(value?: string) {
|
|
457
|
+
return operation(function markdown(ctx) {
|
|
458
|
+
let v = value ?? ctx.ctx
|
|
459
|
+
if (isNullish(v)) {
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
invariant(isString(v), 'Value must be a string - got ' + typeof v)
|
|
463
|
+
return multilineText(v, TRIPLE_QUOTE)(ctx)
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function markdownOrString<A extends string | MarkdownOrString>(): Op<A>
|
|
468
|
+
export function markdownOrString<A>(value: MarkdownOrString): Op<A>
|
|
469
|
+
export function markdownOrString(value?: MarkdownOrString) {
|
|
470
|
+
return operation<MarkdownOrString>(function markdownOrString(ctx) {
|
|
471
|
+
let v = value ?? ctx.ctx
|
|
472
|
+
if (isNullish(v)) {
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
if (typeof v === 'string') {
|
|
476
|
+
return text(v)(ctx)
|
|
477
|
+
}
|
|
478
|
+
if ('md' in v) {
|
|
479
|
+
return markdown(v.md)(ctx)
|
|
480
|
+
}
|
|
481
|
+
if ('txt' in v) {
|
|
482
|
+
return multilineText(v.txt)(ctx)
|
|
483
|
+
}
|
|
484
|
+
throw new Error('Invalid MarkdownOrString value: ' + v)
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* helps to join operations
|
|
490
|
+
*
|
|
491
|
+
* @internal
|
|
492
|
+
*
|
|
493
|
+
* @see spaceBetween
|
|
494
|
+
* @see lines
|
|
495
|
+
* @see foreach
|
|
496
|
+
*/
|
|
497
|
+
export function join<A>(
|
|
498
|
+
params: {
|
|
499
|
+
operations: Op<A> | Ops<A>
|
|
500
|
+
} & JoinOptions<CompositeGeneratorNode>,
|
|
501
|
+
): Op<A> {
|
|
502
|
+
return operation<A>(function joinOp({ ctx, out }) {
|
|
503
|
+
const { operations, ...joinOptions } = params
|
|
504
|
+
const ops = Array.isArray(operations) ? operations : [operations]
|
|
505
|
+
invariant(hasAtLeast(ops, 1), 'At least one operation is required')
|
|
506
|
+
let nested
|
|
507
|
+
if (ops.length === 1) {
|
|
508
|
+
const result = ops[0](fresh(ctx))
|
|
509
|
+
nested = result.out.contents as Array<CompositeGeneratorNode>
|
|
510
|
+
} else {
|
|
511
|
+
nested = pipe(
|
|
512
|
+
eachOnFresh(ctx as A, ops),
|
|
513
|
+
map(n => n.out),
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
nested = filter(nested, hasContentOrNewLine)
|
|
518
|
+
|
|
519
|
+
return out.appendIf(
|
|
520
|
+
nested.length > 0,
|
|
521
|
+
joinToNode(
|
|
522
|
+
nested,
|
|
523
|
+
joinOptions,
|
|
524
|
+
),
|
|
525
|
+
)
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Joins all outputs with a space between
|
|
531
|
+
* @see lines
|
|
532
|
+
*/
|
|
533
|
+
export function spaceBetween<A>(...ops: Ops<A>): Op<A> {
|
|
534
|
+
return join({
|
|
535
|
+
operations: ops,
|
|
536
|
+
suffix: (node, _index, isLast) => {
|
|
537
|
+
return !isLast && hasContent(node) ? ' ' : undefined
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Joins all outputs from the operations with the specified number of new lines after each operation
|
|
544
|
+
*
|
|
545
|
+
* @see body
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* lines(
|
|
550
|
+
* print('name'),
|
|
551
|
+
* print('value'),
|
|
552
|
+
* )
|
|
553
|
+
* // Output:
|
|
554
|
+
* // name
|
|
555
|
+
* // value
|
|
556
|
+
* ```
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```ts
|
|
560
|
+
* lines(2)(
|
|
561
|
+
* print('name'),
|
|
562
|
+
* print('value'),
|
|
563
|
+
* )
|
|
564
|
+
* // Output:
|
|
565
|
+
* // name
|
|
566
|
+
* //
|
|
567
|
+
* // value
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
export function lines<A>(...ops: Ops<A>): Op<A>
|
|
571
|
+
export function lines(linesBetween: number): <A>(...ops: Ops<A>) => Op<A>
|
|
572
|
+
export function lines(...args: any[]) {
|
|
573
|
+
let linesBetween = only(args)
|
|
574
|
+
if (isNumber(linesBetween)) {
|
|
575
|
+
let suffix = fresh(undefined)
|
|
576
|
+
for (let i = 0; i < linesBetween; i++) {
|
|
577
|
+
suffix.out.appendNewLine()
|
|
578
|
+
}
|
|
579
|
+
return (...ops: Ops<any>) => {
|
|
580
|
+
return join({
|
|
581
|
+
operations: ops,
|
|
582
|
+
suffix: (_node, _index, isLast) => {
|
|
583
|
+
return !isLast ? suffix.out : undefined
|
|
584
|
+
},
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const ops = args as Ops<any>
|
|
589
|
+
return join({
|
|
590
|
+
operations: ops,
|
|
591
|
+
appendNewLineIfNotEmpty: true,
|
|
592
|
+
skipNewLineAfterLastItem: true,
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Forwards the context to the operations
|
|
598
|
+
*/
|
|
599
|
+
export function withctx<A>(ctx: A): <B>(...ops: Ops<A>) => Op<B>
|
|
600
|
+
export function withctx<A, B>(ctx: B, op: Op<B>, ...ops: Ops<B>): Op<A>
|
|
601
|
+
export function withctx(...args: unknown[]) {
|
|
602
|
+
const ctx = args[0]
|
|
603
|
+
if (args.length === 1) {
|
|
604
|
+
return (...ops: Ops<any>) =>
|
|
605
|
+
operation(function withctx1({ out }) {
|
|
606
|
+
executeOnCtx({ ctx, out }, ops)
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
const ops = args.slice(1) as Ops<any>
|
|
610
|
+
return operation(function withctx2({ out }) {
|
|
611
|
+
executeOnCtx({ ctx, out }, ops)
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Executes the given operation on the property of the context if it is non-nullish
|
|
617
|
+
* If no operation is provided, prints the property name and property value
|
|
618
|
+
* (use {@link printProperty} if you need print value only)
|
|
619
|
+
*
|
|
620
|
+
* @see printProperty
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```ts
|
|
624
|
+
* withctx({name: 'John'})(
|
|
625
|
+
* property(
|
|
626
|
+
* 'name',
|
|
627
|
+
* spaceBetween(
|
|
628
|
+
* print('Name:'),
|
|
629
|
+
* print()
|
|
630
|
+
* )
|
|
631
|
+
* )
|
|
632
|
+
* )
|
|
633
|
+
* // Output:
|
|
634
|
+
* // Name: John
|
|
635
|
+
* ```
|
|
636
|
+
* @example
|
|
637
|
+
* ```ts
|
|
638
|
+
* withctx({name: 'John'})(
|
|
639
|
+
* property('name')
|
|
640
|
+
* )
|
|
641
|
+
* // Output:
|
|
642
|
+
* // name John
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
export function property<A, P extends keyof A & string>(
|
|
646
|
+
propertyName: P,
|
|
647
|
+
op?: Op<A[P] & {}>, // NonNullable
|
|
648
|
+
): Op<A> {
|
|
649
|
+
return operation(function propertyOp({ ctx, out }) {
|
|
650
|
+
const value = isObjectType(ctx) && hasProp(ctx, propertyName) ? ctx[propertyName] : undefined
|
|
651
|
+
if (value === null || value === undefined) {
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
if (!op) {
|
|
655
|
+
invariant(isPrintable(value), `Property ${propertyName} is not printable "${value}"`)
|
|
656
|
+
out.append(propertyName, ' ', String(value))
|
|
657
|
+
return
|
|
658
|
+
}
|
|
659
|
+
op({ ctx: value, out })
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Prints context's property value
|
|
665
|
+
*
|
|
666
|
+
* @see property
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* ```ts
|
|
670
|
+
* withctx({name: 'John'})(
|
|
671
|
+
* printProperty('name')
|
|
672
|
+
* )
|
|
673
|
+
* // Output:
|
|
674
|
+
* // John
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
export function printProperty<A, P extends keyof A & string>(
|
|
678
|
+
propertyName: P,
|
|
679
|
+
): Op<A> {
|
|
680
|
+
return property(propertyName, print() as AnyOp)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
type IterableValue<T> = T extends Iterable<infer U> ? U : never
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Executes given operation on each item of the iterable context
|
|
687
|
+
* And joins the results with the given options
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* property(
|
|
691
|
+
* 'tags',
|
|
692
|
+
* foreach(
|
|
693
|
+
* print(v => `#${v}`),
|
|
694
|
+
* separateComma()
|
|
695
|
+
* ),
|
|
696
|
+
* )
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
export function foreach<A extends Iterable<any>>(
|
|
700
|
+
op: Op<IterableValue<A>>,
|
|
701
|
+
): Op<A>
|
|
702
|
+
export function foreach<A extends Iterable<any>>(
|
|
703
|
+
op: Op<IterableValue<A>>,
|
|
704
|
+
params: JoinOptions<CompositeGeneratorNode>,
|
|
705
|
+
): Op<A>
|
|
706
|
+
export function foreach<A extends Iterable<any>>(
|
|
707
|
+
op: Op<IterableValue<A>>,
|
|
708
|
+
...ops: Ops<IterableValue<A>>
|
|
709
|
+
): Op<A>
|
|
710
|
+
export function foreach<A extends Iterable<any>>(
|
|
711
|
+
...args:
|
|
712
|
+
| [op: Op<IterableValue<A>>]
|
|
713
|
+
| [op: Op<IterableValue<A>>, join: JoinOptions<CompositeGeneratorNode>]
|
|
714
|
+
| [op: Op<IterableValue<A>>, ...ops: Ops<IterableValue<A>>]
|
|
715
|
+
): Op<A> {
|
|
716
|
+
const [arg1, arg2] = args
|
|
717
|
+
if (args.length === 2 && !isFunction(arg2)) {
|
|
718
|
+
const _op = arg1
|
|
719
|
+
const joinOptions = arg2
|
|
720
|
+
return operation(function foreachSingleOp({ ctx, out }) {
|
|
721
|
+
const items = [] as Array<CompositeGeneratorNode>
|
|
722
|
+
for (const value of ctx) {
|
|
723
|
+
const itemOut = _op(fresh(value)).out
|
|
724
|
+
if (hasContent(itemOut)) {
|
|
725
|
+
items.push(itemOut)
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
out.appendIf(
|
|
729
|
+
items.length > 0,
|
|
730
|
+
joinToNode(
|
|
731
|
+
items,
|
|
732
|
+
joinOptions,
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
const ops = args as Ops<IterableValue<A>>
|
|
738
|
+
return operation(({ ctx, out }) => {
|
|
739
|
+
for (const value of ctx) {
|
|
740
|
+
executeOnCtx({ ctx: value, out }, ops)
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function separateWith(separator: string | GeneratorNode): JoinOptions<CompositeGeneratorNode> {
|
|
746
|
+
return {
|
|
747
|
+
separator,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function separateNewLine(lines = 1): JoinOptions<CompositeGeneratorNode> {
|
|
752
|
+
if (lines > 1) {
|
|
753
|
+
let suffix = fresh(undefined)
|
|
754
|
+
for (let i = 0; i < lines; i++) {
|
|
755
|
+
suffix.out.appendNewLine()
|
|
756
|
+
}
|
|
757
|
+
return separateWith(suffix.out)
|
|
758
|
+
}
|
|
759
|
+
return separateWith(NL)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function separateComma(addNewLine?: boolean): JoinOptions<CompositeGeneratorNode> {
|
|
763
|
+
if (addNewLine) {
|
|
764
|
+
return {
|
|
765
|
+
separator: ',',
|
|
766
|
+
appendNewLineIfNotEmpty: true,
|
|
767
|
+
skipNewLineAfterLastItem: true,
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
separator: ', ',
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Executes given operations on each item of the iterable context
|
|
777
|
+
* And joins the results with a new line
|
|
778
|
+
*/
|
|
779
|
+
export function foreachNewLine<A extends Iterable<any>>(
|
|
780
|
+
...ops: Ops<IterableValue<A>>
|
|
781
|
+
): Op<A> {
|
|
782
|
+
return operation(function foreachNewLineOp({ ctx, out }) {
|
|
783
|
+
const items = [] as Array<CompositeGeneratorNode>
|
|
784
|
+
for (const value of ctx) {
|
|
785
|
+
const itemOut = executeOnFresh(value, ops).out
|
|
786
|
+
if (hasContent(itemOut)) {
|
|
787
|
+
items.push(itemOut)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
out.appendIf(
|
|
791
|
+
items.length > 0,
|
|
792
|
+
joinToNode(
|
|
793
|
+
items,
|
|
794
|
+
separateNewLine(),
|
|
795
|
+
),
|
|
796
|
+
)
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Guards context value with a condition and executes operations if the condition is true
|
|
802
|
+
*/
|
|
803
|
+
export function guard<A, N extends A>(
|
|
804
|
+
condition: (ctx: A) => ctx is N,
|
|
805
|
+
...ops: Ops<N>
|
|
806
|
+
): Op<A>
|
|
807
|
+
export function guard<A, Z extends z.ZodType<any, any, any>>(
|
|
808
|
+
zodSchema: Z,
|
|
809
|
+
...ops: Or<
|
|
810
|
+
A extends z.input<NoInfer<Z>> ? true : false,
|
|
811
|
+
z.input<NoInfer<Z>> extends A ? true : false
|
|
812
|
+
> extends true ? Ops<z.output<NoInfer<Z>>> : ['zod guard mismatch']
|
|
813
|
+
): Op<A>
|
|
814
|
+
export function guard(
|
|
815
|
+
condition: Function | z.ZodSchema,
|
|
816
|
+
...ops: Ops<any>
|
|
817
|
+
) {
|
|
818
|
+
return operation('guard', ({ ctx, out }) => {
|
|
819
|
+
if ('safeParse' in condition) {
|
|
820
|
+
const parsed = condition.safeParse(ctx)
|
|
821
|
+
if (parsed.success) {
|
|
822
|
+
executeOnCtx({ ctx: parsed.data, out }, ops)
|
|
823
|
+
} else {
|
|
824
|
+
throw new Error(`Guard failed: ${z.prettifyError(parsed.error)}`)
|
|
825
|
+
}
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
invariant(typeof condition === 'function')
|
|
829
|
+
if (condition(ctx)) {
|
|
830
|
+
executeOnCtx({ ctx, out }, ops)
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Executes operations on the context if the condition is true
|
|
838
|
+
*/
|
|
839
|
+
export function when<A>(
|
|
840
|
+
condition: (ctx: A) => boolean,
|
|
841
|
+
...ops: Ops<NoInfer<A>>
|
|
842
|
+
): Op<A> {
|
|
843
|
+
return operation(function whenOp({ ctx, out }) {
|
|
844
|
+
if (condition(ctx)) {
|
|
845
|
+
executeOnCtx({ ctx, out }, ops)
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function select<A, B>(
|
|
851
|
+
selector: (value: A) => B,
|
|
852
|
+
...ops: Ops<B & {}> // NonNullable
|
|
853
|
+
): Op<A> {
|
|
854
|
+
return operation(function selectOp({ ctx, out }) {
|
|
855
|
+
const value = selector(ctx)
|
|
856
|
+
if (isNonNullish(value)) {
|
|
857
|
+
executeOnCtx({ ctx: value, out }, ops)
|
|
858
|
+
}
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Creates an execution function that runs operations
|
|
864
|
+
* with new context but using the same output
|
|
865
|
+
*/
|
|
866
|
+
function execToOut(out: Output) {
|
|
867
|
+
return <A>(ctx: A, ...ops: Ops<A>) => executeOnCtx({ ctx, out }, ops)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
type ExecToOut = {
|
|
871
|
+
/**
|
|
872
|
+
* Execute operations using the given context and current output
|
|
873
|
+
* @param ctx The context to use
|
|
874
|
+
* @param ops The operations to execute
|
|
875
|
+
* @returns The updated context and output
|
|
876
|
+
*/
|
|
877
|
+
exec: <A>(ctx: A, ...ops: Ops<A>) => Ctx<A>
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Creates print operation with context based on zod schema
|
|
882
|
+
* @example
|
|
883
|
+
* ```ts
|
|
884
|
+
* const newOp = zodOp(schemas.directRelationExpr)(
|
|
885
|
+
* merge(
|
|
886
|
+
* property(
|
|
887
|
+
* 'source',
|
|
888
|
+
* fqnExpr(),
|
|
889
|
+
* ),
|
|
890
|
+
* print(v => v.isBidirectional ? ' <-> ' : ' -> '),
|
|
891
|
+
* property(
|
|
892
|
+
* 'target',
|
|
893
|
+
* fqnExpr(),
|
|
894
|
+
* ),
|
|
895
|
+
* ),
|
|
896
|
+
* )
|
|
897
|
+
* ```
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* ```ts
|
|
901
|
+
* const whereOperator = zodOp(schemas.whereOperator)(({ ctx, exec }) => {
|
|
902
|
+
* if ('and' in ctx) {
|
|
903
|
+
* return exec(ctx, whereAnd())
|
|
904
|
+
* }
|
|
905
|
+
* if ('or' in ctx) {
|
|
906
|
+
* return exec(ctx, whereOr())
|
|
907
|
+
* }
|
|
908
|
+
* nonexhaustive(ctx)
|
|
909
|
+
* })
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
export function zodOp<Z extends z4.$ZodType<any, any>>(schema: Z) {
|
|
913
|
+
return (operation: (input: Ctx<z.output<Z>> & ExecToOut) => any) => {
|
|
914
|
+
return <A extends z.input<Z> = z.input<Z>>(): Op<A> => {
|
|
915
|
+
return ({ ctx, out }: Ctx<A>): Ctx<A> => {
|
|
916
|
+
const result = z.safeParse(schema, ctx)
|
|
917
|
+
if (result.success) {
|
|
918
|
+
const opres = operation({ ctx: result.data, out, exec: execToOut(out) })
|
|
919
|
+
if (opres instanceof CompositeGeneratorNode) {
|
|
920
|
+
return {
|
|
921
|
+
ctx,
|
|
922
|
+
out: opres,
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (typeof opres === 'function') {
|
|
926
|
+
return {
|
|
927
|
+
ctx,
|
|
928
|
+
out,
|
|
929
|
+
...opres({ ctx, out }),
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { ctx, out }
|
|
933
|
+
}
|
|
934
|
+
throw result.error
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|