@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,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
+ }