@atcute/lex-cli 1.1.2 → 2.0.1

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/src/codegen.ts ADDED
@@ -0,0 +1,738 @@
1
+ import { dirname as getDirname, relative as getRelativePath } from 'node:path/posix';
2
+
3
+ import * as prettier from 'prettier';
4
+
5
+ import type {
6
+ LexArray,
7
+ LexBlob,
8
+ LexiconDoc,
9
+ LexIpldType,
10
+ LexObject,
11
+ LexPrimitive,
12
+ LexRecord,
13
+ LexRefVariant,
14
+ LexXrpcBody,
15
+ LexXrpcParameters,
16
+ LexXrpcProcedure,
17
+ LexXrpcQuery,
18
+ LexXrpcSubscription,
19
+ } from '@atcute/lexicon-doc';
20
+
21
+ export interface SourceFile {
22
+ filename: string;
23
+ code: string;
24
+ }
25
+
26
+ export interface ImportMapping {
27
+ nsid: string[];
28
+ imports: string | ((nsid: string) => { type: 'named' | 'namespace'; from: string });
29
+ }
30
+
31
+ export interface LexiconApiOptions {
32
+ documents: LexiconDoc[];
33
+ mappings: ImportMapping[];
34
+ prettier?: {
35
+ cwd?: string;
36
+ };
37
+ }
38
+
39
+ export interface LexiconApiResult {
40
+ files: SourceFile[];
41
+ }
42
+
43
+ type DocumentMap = Map<string, LexiconDoc>;
44
+ type ImportSet = Set<string>;
45
+
46
+ type Literal = string | number | boolean;
47
+
48
+ const lit: (val: Literal | Literal[]) => string = JSON.stringify;
49
+
50
+ const resolveExternalImport = (nsid: string, mappings: ImportMapping[]): ImportMapping | undefined => {
51
+ return mappings.find((mapping) => {
52
+ return mapping.nsid.some((pattern) => {
53
+ if (pattern.endsWith('.*')) {
54
+ return nsid.startsWith(pattern.slice(0, -1));
55
+ }
56
+
57
+ return nsid === pattern;
58
+ });
59
+ });
60
+ };
61
+
62
+ const PURE = `/*#__PURE__*/`;
63
+
64
+ export const generateLexiconApi = async (opts: LexiconApiOptions): Promise<LexiconApiResult> => {
65
+ const documents = opts.documents.toSorted((a, b) => {
66
+ if (a.id < b.id) {
67
+ return -1;
68
+ }
69
+ if (a.id > b.id) {
70
+ return 1;
71
+ }
72
+
73
+ return 0;
74
+ });
75
+
76
+ const map: DocumentMap = new Map(documents.map((doc) => [doc.id, doc]));
77
+ const files: SourceFile[] = [];
78
+
79
+ for (const doc of documents) {
80
+ const filename = `types/${doc.id.replaceAll('.', '/')}.ts`;
81
+ const file = {
82
+ imports: '',
83
+ rawschemas: '',
84
+ schemadefs: '',
85
+ schemas: '',
86
+ interfaces: '',
87
+ exports: '',
88
+ ambients: '',
89
+ };
90
+
91
+ file.imports += `import type {} from '@atcute/lexicons';\n`;
92
+ file.imports += `import * as v from '@atcute/lexicons/validations';\n`;
93
+
94
+ const imports = new Set<string>();
95
+
96
+ const sortedDefIds = Object.keys(doc.defs).toSorted((a, b) => {
97
+ if (a < b) {
98
+ return -1;
99
+ }
100
+ if (a > b) {
101
+ return 1;
102
+ }
103
+
104
+ return 0;
105
+ });
106
+
107
+ for (const defId of sortedDefIds) {
108
+ const def = doc.defs[defId];
109
+ const defUri = `${doc.id}#${defId}`;
110
+
111
+ const camelcased = toCamelCase(defId);
112
+ const varname = `${camelcased}Schema`;
113
+
114
+ let result: string;
115
+ switch (def.type) {
116
+ case 'query': {
117
+ result = generateXrpcQuery(imports, defUri, def);
118
+
119
+ file.imports += `import type {} from '@atcute/lexicons/ambient';\n`;
120
+
121
+ file.ambients += `declare module '@atcute/lexicons/ambient' {\n`;
122
+ file.ambients += ` interface XRPCQueries {\n`;
123
+ file.ambients += ` ${lit(stripMainHash(defUri))}: ${camelcased}Schema;\n`;
124
+ file.ambients += ` }\n`;
125
+ file.ambients += `}`;
126
+ break;
127
+ }
128
+ case 'procedure': {
129
+ result = generateXrpcProcedure(imports, defUri, def);
130
+
131
+ file.imports += `import type {} from '@atcute/lexicons/ambient';\n`;
132
+
133
+ file.ambients += `declare module '@atcute/lexicons/ambient' {\n`;
134
+ file.ambients += ` interface XRPCProcedures {\n`;
135
+ file.ambients += ` ${lit(stripMainHash(defUri))}: ${camelcased}Schema;\n`;
136
+ file.ambients += ` }\n`;
137
+ file.ambients += `}`;
138
+ break;
139
+ }
140
+ case 'subscription': {
141
+ result = generateXrpcSubscription(imports, defUri, def);
142
+
143
+ file.imports += `import type {} from '@atcute/lexicons/ambient';\n`;
144
+
145
+ file.ambients += `declare module '@atcute/lexicons/ambient' {\n`;
146
+ file.ambients += ` interface XRPCSubscriptions {\n`;
147
+ file.ambients += ` ${lit(stripMainHash(defUri))}: ${camelcased}Schema;\n`;
148
+ file.ambients += ` }\n`;
149
+ file.ambients += `}`;
150
+ break;
151
+ }
152
+ case 'object': {
153
+ result = generateObject(imports, defUri, def);
154
+ break;
155
+ }
156
+ case 'record': {
157
+ result = generateRecord(imports, defUri, def);
158
+
159
+ file.imports += `import type {} from '@atcute/lexicons/ambient';\n`;
160
+
161
+ file.ambients += `declare module '@atcute/lexicons/ambient' {\n`;
162
+ file.ambients += ` interface Records {\n`;
163
+ file.ambients += ` ${lit(stripMainHash(defUri))}: ${camelcased}Schema;\n`;
164
+ file.ambients += ` }\n`;
165
+ file.ambients += `}`;
166
+ break;
167
+ }
168
+ case 'token': {
169
+ result = `${PURE} v.literal(${lit(stripMainHash(defUri))})`;
170
+ break;
171
+ }
172
+ default: {
173
+ result = generateType(imports, defUri, def);
174
+ break;
175
+ }
176
+ }
177
+
178
+ file.rawschemas += `const _${varname} = ${result};\n`;
179
+
180
+ file.schemadefs += `type ${camelcased}$schematype = typeof _${varname};\n`;
181
+
182
+ file.schemas += `export interface ${camelcased}Schema extends ${camelcased}$schematype {}\n`;
183
+
184
+ file.exports += `export const ${varname} = _${varname} as ${camelcased}Schema;\n`;
185
+
186
+ switch (def.type) {
187
+ case 'array':
188
+ case 'object':
189
+ case 'record':
190
+ case 'unknown': {
191
+ file.interfaces += `export interface ${toTitleCase(defId)} extends v.InferInput<typeof ${varname}> {}\n`;
192
+ break;
193
+ }
194
+ case 'blob':
195
+ case 'boolean':
196
+ case 'bytes':
197
+ case 'cid-link':
198
+ case 'integer':
199
+ case 'string':
200
+ case 'token': {
201
+ file.interfaces += `export type ${toTitleCase(defId)} = v.InferInput<typeof ${varname}>;\n`;
202
+ break;
203
+ }
204
+ }
205
+ }
206
+
207
+ {
208
+ const dirname = getDirname(filename);
209
+
210
+ const sortedImports = [...imports].toSorted((a, b) => {
211
+ if (a < b) {
212
+ return -1;
213
+ }
214
+ if (a > b) {
215
+ return 1;
216
+ }
217
+
218
+ return 0;
219
+ });
220
+
221
+ for (const ns of sortedImports) {
222
+ const local = map.get(ns);
223
+
224
+ if (local) {
225
+ const target = `types/${ns.replaceAll('.', '/')}.js`;
226
+
227
+ let relative = getRelativePath(dirname, target);
228
+ if (!relative.startsWith('.')) {
229
+ relative = `./${relative}`;
230
+ }
231
+
232
+ file.imports += `import * as ${toTitleCase(ns)} from ${lit(relative)};\n`;
233
+ continue;
234
+ }
235
+
236
+ const external = resolveExternalImport(ns, opts.mappings);
237
+
238
+ if (external) {
239
+ if (typeof external.imports === 'function') {
240
+ const res = external.imports(ns);
241
+
242
+ if (res.type === 'named') {
243
+ file.imports += `import { ${toTitleCase(ns)} } from ${lit(res.from)};\n`;
244
+ } else if (res.type === 'namespace') {
245
+ file.imports += `import * as ${toTitleCase(ns)} from ${lit(res.from)};\n`;
246
+ }
247
+ } else {
248
+ file.imports += `import { ${toTitleCase(ns)} } from ${lit(external.imports)};\n`;
249
+ }
250
+
251
+ continue;
252
+ }
253
+
254
+ throw new Error(`'${doc.id}' referenced non-existent '${ns}' namespace`);
255
+ }
256
+ }
257
+
258
+ files.push({
259
+ filename: filename,
260
+ code:
261
+ file.imports +
262
+ `\n\n` +
263
+ file.rawschemas +
264
+ `\n\n` +
265
+ file.schemadefs +
266
+ `\n\n` +
267
+ file.schemas +
268
+ `\n\n` +
269
+ file.exports +
270
+ `\n\n` +
271
+ file.interfaces +
272
+ `\n\n` +
273
+ file.ambients,
274
+ });
275
+ }
276
+
277
+ {
278
+ let code = ``;
279
+
280
+ for (const doc of map.values()) {
281
+ code += `export * as ${toTitleCase(doc.id)} from ${lit(`./types/${doc.id.replaceAll('.', '/')}.js`)};\n`;
282
+ }
283
+
284
+ files.push({
285
+ filename: 'index.ts',
286
+ code: code,
287
+ });
288
+ }
289
+
290
+ if (opts.prettier) {
291
+ const config = await prettier.resolveConfig(opts.prettier.cwd ?? process.cwd(), { editorconfig: true });
292
+
293
+ for (const file of files) {
294
+ const formatted = await prettier.format(file.code, { ...config, parser: 'typescript' });
295
+ file.code = formatted;
296
+ }
297
+ }
298
+
299
+ return { files };
300
+ };
301
+
302
+ const generateXrpcQuery = (imports: ImportSet, defUri: string, spec: LexXrpcQuery): string => {
303
+ const params = generateXrpcParameters(imports, defUri, spec.parameters);
304
+ const output = generateXrpcBody(imports, defUri, spec.output);
305
+
306
+ return `${PURE} v.query(${lit(stripMainHash(defUri))}, {\n"params": ${params}, "output": ${output} })`;
307
+ };
308
+
309
+ const generateXrpcProcedure = (imports: ImportSet, defUri: string, spec: LexXrpcProcedure): string => {
310
+ const params = generateXrpcParameters(imports, defUri, spec.parameters);
311
+ const input = generateXrpcBody(imports, defUri, spec.input);
312
+ const output = generateXrpcBody(imports, defUri, spec.output);
313
+
314
+ return `${PURE} v.procedure(${lit(stripMainHash(defUri))}, {\n"params": ${params}, "input": ${input}, "output": ${output} })`;
315
+ };
316
+
317
+ const generateXrpcSubscription = (imports: ImportSet, defUri: string, spec: LexXrpcSubscription): string => {
318
+ const schema = spec.message?.schema;
319
+
320
+ const params = generateXrpcParameters(imports, defUri, spec.parameters);
321
+
322
+ let inner = ``;
323
+
324
+ inner += `"params": ${params},`;
325
+
326
+ if (schema) {
327
+ if (schema.type === 'object') {
328
+ const res = generateObject(imports, defUri, schema, 'none');
329
+
330
+ inner += `"message": ${res},`;
331
+ } else {
332
+ const res = generateType(imports, defUri, schema);
333
+
334
+ inner += `get "message" () { return ${res} },`;
335
+ }
336
+ } else {
337
+ inner += `"message": null,`;
338
+ }
339
+
340
+ return `${PURE} v.subscription(${lit(stripMainHash(defUri))}, {\n${inner}})`;
341
+ };
342
+
343
+ const generateXrpcBody = (imports: ImportSet, defUri: string, spec: LexXrpcBody | undefined): string => {
344
+ if (spec === undefined) {
345
+ return `null`;
346
+ }
347
+
348
+ const schema = spec.schema;
349
+ const encoding = spec.encoding;
350
+
351
+ if (schema) {
352
+ let inner = ``;
353
+
354
+ inner += `"type": "lex",`;
355
+
356
+ if (schema.type === 'object') {
357
+ const res = generateObject(imports, defUri, schema, 'none');
358
+
359
+ inner += `"schema": ${res},`;
360
+ } else {
361
+ const res = generateType(imports, defUri, schema);
362
+
363
+ inner += `get "schema" () { return ${res} },`;
364
+ }
365
+
366
+ return `{\n${inner}}`;
367
+ }
368
+
369
+ if (encoding) {
370
+ const types = encoding.split(',').map((type) => type.trim());
371
+
372
+ let inner = ``;
373
+
374
+ inner += `"type": "blob",`;
375
+
376
+ if (types.length > 1 || types[0] !== '*/*') {
377
+ inner += `"encoding": ${lit(types)},`;
378
+ }
379
+
380
+ return `{\n${inner}}`;
381
+ }
382
+
383
+ return `null`;
384
+ };
385
+
386
+ const generateXrpcParameters = (
387
+ imports: ImportSet,
388
+ defUri: string,
389
+ spec: LexXrpcParameters | undefined,
390
+ ): string => {
391
+ if (spec === undefined) {
392
+ return `null`;
393
+ }
394
+
395
+ const mask: LexObject = {
396
+ type: 'object',
397
+ description: spec.description,
398
+ required: spec.required,
399
+ properties: spec.properties,
400
+ };
401
+
402
+ return generateObject(imports, defUri, mask, 'none');
403
+ };
404
+
405
+ const generateRecord = (imports: ImportSet, defUri: string, spec: LexRecord): string => {
406
+ const schema = generateObject(imports, defUri, spec.record, 'required');
407
+
408
+ let key = `${PURE} v.string()`;
409
+ if (spec.key) {
410
+ if (spec.key === 'tid') {
411
+ key = `${PURE} v.tidString()`;
412
+ } else if (spec.key === 'nsid') {
413
+ key = `${PURE} v.nsidString()`;
414
+ } else if (spec.key.startsWith('literal:')) {
415
+ key = `${PURE} v.literal(${lit(spec.key.slice('literal:'.length))})`;
416
+ }
417
+ }
418
+
419
+ return `${PURE} v.record(${key}, ${schema})`;
420
+ };
421
+
422
+ const generateObject = (
423
+ imports: ImportSet,
424
+ defUri: string,
425
+ spec: LexObject,
426
+ writeType: 'required' | 'optional' | 'none' = 'optional',
427
+ ): string => {
428
+ const required = new Set(spec.required);
429
+ const nullable = new Set(spec.nullable);
430
+
431
+ let inner = ``;
432
+
433
+ switch (writeType) {
434
+ case 'optional': {
435
+ inner += `"$type": ${PURE} v.optional(${PURE} v.literal(${lit(stripMainHash(defUri))})),`;
436
+ break;
437
+ }
438
+ case 'required': {
439
+ inner += `"$type": ${PURE} v.literal(${lit(stripMainHash(defUri))}),`;
440
+ break;
441
+ }
442
+ }
443
+
444
+ for (const [prop, propSpec] of Object.entries(spec.properties ?? {})) {
445
+ const lazy = isRefVariant(propSpec.type === 'array' ? propSpec.items : propSpec);
446
+ const optional = !required.has(prop) && !('default' in propSpec);
447
+ const nulled = nullable.has(prop);
448
+
449
+ let call = generateType(imports, defUri, propSpec, lazy);
450
+
451
+ if (nulled) {
452
+ call = `${PURE} v.nullable(${call})`;
453
+ }
454
+
455
+ if (optional) {
456
+ call = `${PURE} v.optional(${call})`;
457
+ }
458
+
459
+ if (lazy) {
460
+ inner += `get ${lit(prop)} () { return ${call} },`;
461
+ } else {
462
+ inner += `${lit(prop)}: ${call},`;
463
+ }
464
+ }
465
+
466
+ return `${PURE} v.object({\n${inner}})`;
467
+ };
468
+
469
+ const generateType = (
470
+ imports: ImportSet,
471
+ defUri: string,
472
+ spec: LexArray | LexPrimitive | LexIpldType | LexRefVariant | LexBlob,
473
+ lazy = false,
474
+ ): string => {
475
+ switch (spec.type) {
476
+ // LexRefVariant
477
+ case 'ref': {
478
+ const ref = spec.ref;
479
+
480
+ if (ref.startsWith('#')) {
481
+ const id = ref.slice(1);
482
+
483
+ return `${toCamelCase(id)}Schema`;
484
+ } else {
485
+ const [ns, id = 'main'] = ref.split('#');
486
+ if (ns === stripHash(defUri)) {
487
+ return `${toCamelCase(id)}Schema`;
488
+ }
489
+
490
+ imports.add(ns);
491
+
492
+ return `${toTitleCase(ns)}.${toCamelCase(id)}Schema`;
493
+ }
494
+ }
495
+ case 'union': {
496
+ const refs = spec.refs.map((ref): string => {
497
+ if (ref.startsWith('#')) {
498
+ const id = ref.slice(1);
499
+
500
+ return `${toCamelCase(id)}Schema`;
501
+ } else {
502
+ const [ns, id = 'main'] = ref.split('#');
503
+ if (ns === stripHash(defUri)) {
504
+ return `${toCamelCase(id)}Schema`;
505
+ }
506
+
507
+ imports.add(ns);
508
+
509
+ return `${toTitleCase(ns)}.${toCamelCase(id)}Schema`;
510
+ }
511
+ });
512
+
513
+ return `${PURE} v.variant([${refs.join(', ')}]${spec.closed ? `, true` : ``})`;
514
+ }
515
+
516
+ // LexArray
517
+ case 'array': {
518
+ let item = generateType(imports, defUri, spec.items);
519
+ if (!lazy && (spec.items.type === 'ref' || spec.items.type === 'union')) {
520
+ item = `(() => { return ${item}; })`;
521
+ }
522
+
523
+ let pipe: string[] = [];
524
+
525
+ if ((spec.minLength ?? 0) > 0 || spec.maxLength !== undefined) {
526
+ if (spec.maxLength === undefined) {
527
+ pipe.push(`${PURE} v.arrayLength(${lit(spec.minLength ?? 0)})`);
528
+ } else {
529
+ pipe.push(`${PURE} v.arrayLength(${lit(spec.minLength ?? 0)}, ${lit(spec.maxLength)})`);
530
+ }
531
+ }
532
+
533
+ let call = `${PURE} v.array(${item})`;
534
+
535
+ if (pipe.length !== 0) {
536
+ call = `${PURE} v.constrain(${call}, [ ${pipe.join(', ')} ])`;
537
+ }
538
+
539
+ return call;
540
+ }
541
+
542
+ // LexPrimitive
543
+ case 'boolean': {
544
+ if (spec.const !== undefined) {
545
+ return `${PURE} v.literal(${spec.const})`;
546
+ }
547
+
548
+ let call = `${PURE} v.boolean()`;
549
+
550
+ if (spec.default !== undefined) {
551
+ call = `${PURE} v.optional(${call}, ${lit(spec.default)})`;
552
+ }
553
+
554
+ return call;
555
+ }
556
+ case 'integer': {
557
+ if (spec.const !== undefined) {
558
+ return `${PURE} v.literal(${lit(spec.const)})`;
559
+ }
560
+
561
+ if (spec.enum !== undefined) {
562
+ return `${PURE} v.literalEnum(${lit(spec.enum)})`;
563
+ }
564
+
565
+ let pipe: string[] = [];
566
+
567
+ if ((spec.minimum ?? 0) > 0 || spec.maximum !== undefined) {
568
+ if (spec.maximum === undefined) {
569
+ pipe.push(`${PURE} v.integerRange(${lit(spec.minimum ?? 0)})`);
570
+ } else {
571
+ pipe.push(`${PURE} v.integerRange(${lit(spec.minimum ?? 0)}, ${lit(spec.maximum)})`);
572
+ }
573
+ }
574
+
575
+ let call = `${PURE} v.integer()`;
576
+
577
+ if (pipe.length !== 0) {
578
+ call = `${PURE} v.constrain(${call}, [ ${pipe.join(', ')} ])`;
579
+ }
580
+
581
+ if (spec.default !== undefined) {
582
+ call = `${PURE} v.optional(${call}, ${lit(spec.default)})`;
583
+ }
584
+
585
+ return call;
586
+ }
587
+ case 'string': {
588
+ if (spec.const !== undefined) {
589
+ return `${PURE} v.literal(${lit(spec.const)})`;
590
+ }
591
+
592
+ if (spec.enum !== undefined) {
593
+ return `${PURE} v.literalEnum(${lit(spec.enum)})`;
594
+ }
595
+
596
+ let pipe: string[] = [];
597
+
598
+ if ((spec.minLength ?? 0) > 0 || spec.maxLength !== undefined) {
599
+ if (spec.maxLength === undefined) {
600
+ pipe.push(`${PURE} v.stringLength(${lit(spec.minLength ?? 0)})`);
601
+ } else {
602
+ pipe.push(`${PURE} v.stringLength(${lit(spec.minLength ?? 0)}, ${lit(spec.maxLength)})`);
603
+ }
604
+ }
605
+
606
+ if ((spec.minGraphemes ?? 0) > 0 || spec.maxGraphemes !== undefined) {
607
+ if (spec.maxGraphemes === undefined) {
608
+ pipe.push(`${PURE} v.stringGraphemes(${lit(spec.minGraphemes ?? 0)})`);
609
+ } else {
610
+ pipe.push(`${PURE} v.stringGraphemes(${lit(spec.minGraphemes ?? 0)}, ${lit(spec.maxGraphemes)})`);
611
+ }
612
+ }
613
+
614
+ let call = `${PURE} v.string()`;
615
+
616
+ if (spec.knownValues?.length) {
617
+ call = `${PURE} v.string<${spec.knownValues.map(lit).join(' | ')} | (string & {})>()`;
618
+ }
619
+
620
+ switch (spec.format) {
621
+ case 'at-identifier': {
622
+ call = `${PURE} v.actorIdentifierString()`;
623
+ break;
624
+ }
625
+ case 'at-uri': {
626
+ call = `${PURE} v.resourceUriString()`;
627
+ break;
628
+ }
629
+ case 'datetime': {
630
+ call = `${PURE} v.datetimeString()`;
631
+ break;
632
+ }
633
+ case 'did': {
634
+ call = `${PURE} v.didString()`;
635
+ break;
636
+ }
637
+ case 'handle': {
638
+ call = `${PURE} v.handleString()`;
639
+ break;
640
+ }
641
+ case 'language': {
642
+ call = `${PURE} v.languageCodeString()`;
643
+ break;
644
+ }
645
+ case 'nsid': {
646
+ call = `${PURE} v.nsidString()`;
647
+ break;
648
+ }
649
+ case 'record-key': {
650
+ call = `${PURE} v.recordKeyString()`;
651
+ break;
652
+ }
653
+ case 'tid': {
654
+ call = `${PURE} v.tidString()`;
655
+ break;
656
+ }
657
+ case 'uri': {
658
+ call = `${PURE} v.genericUriString()`;
659
+ break;
660
+ }
661
+ }
662
+
663
+ if (pipe.length !== 0) {
664
+ call = `${PURE} v.constrain(${call}, [ ${pipe.join(', ')} ])`;
665
+ }
666
+
667
+ if (spec.default !== undefined) {
668
+ call = `${PURE} v.optional(${call}, ${lit(spec.default)})`;
669
+ }
670
+
671
+ return call;
672
+ }
673
+ case 'unknown': {
674
+ return `${PURE} v.unknown()`;
675
+ }
676
+
677
+ // LexBlob
678
+ case 'blob': {
679
+ return `${PURE} v.blob()`;
680
+ }
681
+
682
+ // LexIpldType
683
+ case 'bytes': {
684
+ let pipe: string[] = [];
685
+
686
+ if ((spec.minLength ?? 0) > 0 || spec.maxLength !== undefined) {
687
+ if (spec.maxLength === undefined) {
688
+ pipe.push(`${PURE} v.bytesSize(${lit(spec.minLength ?? 0)})`);
689
+ } else {
690
+ pipe.push(`${PURE} v.bytesSize(${lit(spec.minLength ?? 0)}, ${lit(spec.maxLength)})`);
691
+ }
692
+ }
693
+
694
+ let call = `${PURE} v.bytes()`;
695
+
696
+ if (pipe.length !== 0) {
697
+ call = `${PURE} v.constrain(${call}, [ ${pipe.join(', ')} ])`;
698
+ }
699
+
700
+ return call;
701
+ }
702
+ case 'cid-link': {
703
+ return `${PURE} v.cidLink()`;
704
+ }
705
+ }
706
+ };
707
+
708
+ const isRefVariant = (
709
+ spec: LexArray | LexPrimitive | LexIpldType | LexRefVariant | LexBlob,
710
+ ): spec is LexRefVariant => {
711
+ const type = spec.type;
712
+ return type === 'ref' || type === 'union';
713
+ };
714
+
715
+ const stripHash = (defUri: string): string => {
716
+ const index = defUri.indexOf('#');
717
+ if (index === -1) {
718
+ return defUri;
719
+ }
720
+
721
+ return defUri.slice(0, index);
722
+ };
723
+
724
+ const stripMainHash = (defUri: string): string => {
725
+ return defUri.endsWith('#main') ? defUri.slice(0, -'#main'.length) : defUri;
726
+ };
727
+
728
+ const toTitleCase = (v: string): string => {
729
+ v = v.replace(/^([a-z])/gi, (_, g) => g.toUpperCase());
730
+ v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase());
731
+ return v.replace(/[.-]/g, '');
732
+ };
733
+
734
+ const toCamelCase = (v: string): string => {
735
+ v = v.replace(/^([A-Z])/gi, (_, g) => g.toLowerCase());
736
+ v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase());
737
+ return v.replace(/[.-]/g, '');
738
+ };