tangled
alpha
login
or
join now
danabra.mov
/
typelex
56
fork
atom
An experimental TypeSpec syntax for Lexicon
56
fork
atom
overview
issues
1
pulls
2
pipelines
simp
danabra.mov
5 months ago
62c241c1
f582a30e
+479
-1183
1 changed file
expand all
collapse all
unified
split
packages
emitter
src
emitter.ts
+479
-1183
packages/emitter/src/emitter.ts
···
78
private processNamespace(ns: any) {
79
const fullName = getNamespaceFullName(ns);
80
81
-
// Skip built-in TypeSpec namespaces but still process their children
82
if (fullName && !fullName.startsWith("TypeSpec")) {
83
-
// Check if this namespace should be a lexicon file
84
const hasModels = ns.models.size > 0;
85
const hasScalars = ns.scalars.size > 0;
86
const hasUnions = ns.unions?.size > 0;
87
const hasOperations = ns.operations?.size > 0;
88
const hasChildNamespaces = ns.namespaces.size > 0;
89
-
90
-
// Heuristic: if namespace has models/scalars/unions but no operations and no child namespaces
91
-
const shouldEmitLexicon =
92
-
(hasModels || hasScalars || hasUnions) &&
93
-
!hasOperations &&
94
-
!hasChildNamespaces;
95
-
96
-
if (shouldEmitLexicon) {
97
-
const models = [...ns.models.values()];
98
-
const lexiconId = fullName;
99
-
const isDefsFile = fullName.endsWith(".defs");
100
-
101
-
// Find Main model (if not a .defs file)
102
-
const mainModel = isDefsFile
103
-
? null
104
-
: models.find((m) => m.name === "Main");
105
-
const otherModels = models.filter((m) => m.name !== "Main");
106
-
107
-
// Case 1: Namespace ends with .defs -> shared defs file (no main)
108
-
if (isDefsFile) {
109
-
this.currentLexiconId = lexiconId;
110
-
111
-
const lexicon: LexiconDocument = {
112
-
lexicon: 1,
113
-
id: lexiconId,
114
-
defs: {},
115
-
};
116
-
117
-
const nsDescription = getDoc(this.program, ns);
118
-
if (nsDescription) {
119
-
lexicon.description = nsDescription;
120
-
}
121
-
122
-
// All models go into defs
123
-
for (const model of models) {
124
-
this.addModelToDefs(lexicon, model);
125
-
}
126
-
127
-
// All scalars go into defs
128
-
for (const [_, scalar] of ns.scalars) {
129
-
this.addScalarToDefs(lexicon, scalar);
130
-
}
131
-
132
-
// All unions go into defs
133
-
if (ns.unions) {
134
-
for (const [_, union] of ns.unions) {
135
-
this.addUnionToDefs(lexicon, union);
136
-
}
137
-
}
138
-
139
-
this.lexicons.set(lexiconId, lexicon);
140
-
this.currentLexiconId = null;
141
-
}
142
-
// Case 2: Model named "Main" -> lexicon with main + other defs
143
-
else if (mainModel) {
144
-
this.currentLexiconId = lexiconId;
145
-
146
-
// Explicit description rules:
147
-
// - Namespace @doc -> lexicon.description
148
-
// - Main model @doc -> defs.main.description
149
-
const nsDescription = getDoc(this.program, ns);
150
-
const modelDescription = getDoc(this.program, mainModel);
151
-
152
-
// Check if this is a record type
153
-
const recordKey = getRecordKey(this.program, mainModel);
154
-
155
-
// For Main model, always include its description on the def
156
-
const modelDef = this.modelToLexiconObject(
157
-
mainModel,
158
-
modelDescription !== undefined,
159
-
);
160
-
let mainDef: any = modelDef;
161
-
162
-
if (recordKey) {
163
-
// Wrap the object in a record structure
164
-
mainDef = {
165
-
type: "record",
166
-
key: recordKey,
167
-
record: modelDef,
168
-
};
169
-
170
-
// For records, move description from record object to record def
171
-
if (modelDescription) {
172
-
mainDef.description = modelDescription;
173
-
delete modelDef.description;
174
-
}
175
-
}
176
-
177
-
const lexicon: LexiconDocument = {
178
-
lexicon: 1,
179
-
id: lexiconId,
180
-
defs: {
181
-
main: mainDef,
182
-
},
183
-
};
184
-
185
-
// Namespace description always goes to lexicon level
186
-
if (nsDescription) {
187
-
lexicon.description = nsDescription;
188
-
}
189
-
190
-
// Add other models as additional defs in same file
191
-
for (const model of otherModels) {
192
-
this.addModelToDefs(lexicon, model);
193
-
}
194
195
-
// Add scalars as defs
196
-
for (const [_, scalar] of ns.scalars) {
197
-
this.addScalarToDefs(lexicon, scalar);
198
-
}
199
-
200
-
// Add unions as defs
201
-
if (ns.unions) {
202
-
for (const [_, union] of ns.unions) {
203
-
this.addUnionToDefs(lexicon, union);
204
-
}
205
-
}
206
207
-
this.lexicons.set(lexiconId, lexicon);
208
-
this.currentLexiconId = null;
209
-
}
210
-
// Case 3: No Main model and not .defs -> error
211
-
else {
212
-
// Namespace has models/scalars but no Main model and doesn't end with .defs
213
-
// This is an error - user must be explicit
214
-
throw new Error(
215
-
`Namespace "${fullName}" has models/scalars but no Main model. ` +
216
-
`Either add a "model Main" or rename namespace to "${fullName}.defs" for shared definitions.`,
217
-
);
218
-
}
219
-
} else if (
220
-
(hasModels || hasScalars || hasUnions) &&
221
-
!hasOperations &&
222
-
hasChildNamespaces
223
-
) {
224
-
// Namespace has both models and child namespaces
225
-
// Only create a .defs file if the namespace doesn't already end with .defs
226
-
const isDefsNs = fullName.endsWith(".defs");
227
-
const lexiconId = isDefsNs ? fullName : fullName + ".defs";
228
-
this.currentLexiconId = lexiconId;
229
230
-
const lexicon: LexiconDocument = {
231
-
lexicon: 1,
232
-
id: lexiconId,
233
-
defs: {},
234
-
};
235
236
-
const nsDescription = getDoc(this.program, ns);
237
-
if (nsDescription) {
238
-
lexicon.description = nsDescription;
239
-
}
0
0
240
241
-
for (const [_, model] of ns.models) {
242
-
this.addModelToDefs(lexicon, model);
243
-
}
244
245
-
for (const [_, scalar] of ns.scalars) {
246
-
this.addScalarToDefs(lexicon, scalar);
247
-
}
0
0
0
248
249
-
if (ns.unions) {
250
-
for (const [_, union] of ns.unions) {
251
-
this.addUnionToDefs(lexicon, union);
252
-
}
253
-
}
254
255
-
this.lexicons.set(lexiconId, lexicon);
256
-
this.currentLexiconId = null;
257
-
} else if (hasOperations) {
258
-
// Process operations for queries and procedures
259
-
const lexiconId = fullName!;
260
-
const lexicon: LexiconDocument = {
261
-
lexicon: 1,
262
-
id: lexiconId,
263
-
defs: {},
264
-
};
265
266
-
this.currentLexiconId = lexiconId;
0
0
267
268
-
// Check if there's a Main operation
269
-
let mainOp = null;
270
-
for (const [name, operation] of ns.operations) {
271
-
if (name === "main" || name === "Main") {
272
-
mainOp = operation;
273
-
break;
274
-
}
275
-
}
276
277
-
if (mainOp) {
278
-
this.addOperationToDefs(lexicon, mainOp, "main");
279
-
}
280
281
-
// Add other operations as defs
282
-
for (const [name, operation] of ns.operations) {
283
-
if (name !== "main" && name !== "Main") {
284
-
this.addOperationToDefs(lexicon, operation, name);
285
-
}
286
-
}
287
288
-
// Also add any models in this namespace (like getLikes.Like)
289
-
for (const [_, model] of ns.models) {
290
-
if (model.name !== "Main") {
291
-
this.addModelToDefs(lexicon, model);
292
-
}
293
-
}
294
295
-
// Add scalars
296
-
for (const [_, scalar] of ns.scalars) {
297
-
this.addScalarToDefs(lexicon, scalar);
298
-
}
0
0
299
300
-
// Add unions
301
-
if (ns.unions) {
302
-
for (const [_, union] of ns.unions) {
303
-
this.addUnionToDefs(lexicon, union);
304
-
}
305
-
}
306
307
-
this.lexicons.set(lexiconId, lexicon);
308
-
this.currentLexiconId = null;
0
0
0
309
}
0
310
}
311
312
-
// Always recursively process child namespaces
313
-
for (const [_, childNs] of ns.namespaces) {
314
-
this.processNamespace(childNs);
0
0
0
0
0
0
0
0
0
0
0
315
}
316
}
317
318
-
/**
319
-
* Add a model to a lexicon's defs object
320
-
*/
321
private addModelToDefs(lexicon: LexiconDocument, model: Model) {
322
-
// Validate PascalCase convention for model names
323
-
if (model.name && model.name[0] !== model.name[0].toUpperCase()) {
324
this.program.reportDiagnostic({
325
code: "invalid-model-name",
326
severity: "error",
···
330
return;
331
}
332
333
-
const defName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
334
335
-
// Skip error models - they're not emitted as defs
336
-
if (isErrorModel(this.program, model)) {
337
-
return;
338
-
}
339
340
-
// Check if this is a token type
341
if (isToken(this.program, model)) {
342
-
const tokenDef: any = {
343
-
type: "token",
344
-
};
345
-
346
-
const description = getDoc(this.program, model);
347
-
if (description) {
348
-
tokenDef.description = description;
349
-
}
350
-
351
-
lexicon.defs[defName] = tokenDef;
352
return;
353
}
354
355
-
// Check if this model is actually an array type (via `is` declaration)
356
-
// e.g., `model Preferences is SomeUnion[]`
357
if (isArrayModelType(this.program, model)) {
358
const arrayDef = this.modelToLexiconArray(model);
359
if (arrayDef) {
360
-
const description = getDoc(this.program, model);
361
-
if (description && !arrayDef.description) {
362
-
arrayDef.description = description;
363
-
}
364
-
lexicon.defs[defName] = arrayDef;
365
return;
366
}
367
}
368
369
const modelDef = this.modelToLexiconObject(model);
370
-
371
-
const description = getDoc(this.program, model);
372
-
if (description && !modelDef.description) {
373
-
modelDef.description = description;
374
-
}
375
-
376
-
lexicon.defs[defName] = modelDef;
377
}
378
379
-
/**
380
-
* Add a scalar to a lexicon's defs object
381
-
*/
382
private addScalarToDefs(lexicon: LexiconDocument, scalar: Scalar) {
383
-
// Skip built-in TypeSpec scalars
384
-
if (scalar.namespace && scalar.namespace.name === "TypeSpec") {
385
-
return;
386
-
}
387
-
388
-
// Skip scalars that extend built-in types - they should be inlined
389
-
// Only add scalars that are explicitly marked as atproto format scalars
390
-
const baseScalar = scalar.baseScalar;
391
-
if (baseScalar && baseScalar.namespace?.name === "TypeSpec") {
392
-
// This scalar just adds constraints to a built-in type
393
-
// Don't emit as a separate def - will be inlined where used
394
-
return;
395
-
}
396
397
const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1);
398
-
399
-
// Convert scalar to lexicon primitive/string def
400
-
const scalarDef: any = this.scalarToLexiconPrimitive(scalar, undefined);
401
-
402
const description = getDoc(this.program, scalar);
403
-
if (description && !scalarDef.description) {
404
-
scalarDef.description = description;
405
-
}
406
-
407
-
lexicon.defs[defName] = scalarDef;
408
}
409
410
-
/**
411
-
* Add a union to a lexicon's defs object
412
-
*/
413
private addUnionToDefs(lexicon: LexiconDocument, union: Union) {
414
-
const defName =
415
-
(union as any).name?.charAt(0).toLowerCase() +
416
-
(union as any).name?.slice(1);
417
-
if (!defName) return;
418
419
-
// Convert union to lexicon def - pass true to prevent self-reference
420
const unionDef: any = this.typeToLexiconDefinition(union, undefined, true);
421
if (!unionDef) return;
422
423
-
const description = getDoc(this.program, union);
0
0
0
0
0
424
425
-
// Emit union def if it's:
426
-
// 1. A union of model refs (type: "union")
427
-
// 2. A string primitive with knownValues (string literals + string type)
428
-
if (unionDef.type === "union" || (unionDef.type === "string" && (unionDef as any).knownValues)) {
429
-
if (description && !unionDef.description) {
430
-
unionDef.description = description;
431
-
}
432
-
lexicon.defs[defName] = unionDef;
433
}
0
434
}
435
436
-
private addOperationToDefs(
437
-
lexicon: LexiconDocument,
438
-
operation: any,
439
-
defName: string,
440
-
) {
441
-
const description = getDoc(this.program, operation);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
442
443
-
if (isQuery(this.program, operation)) {
444
-
// Build query definition
445
-
const queryDef: any = {
446
-
type: "query",
447
-
};
448
449
-
if (description) {
450
-
queryDef.description = description;
0
451
}
0
452
453
-
// Handle parameters - only include if operation has actual parameters
454
-
if (
455
-
operation.parameters &&
456
-
operation.parameters.properties &&
457
-
operation.parameters.properties.size > 0
458
-
) {
459
-
const params: any = {
460
-
type: "params",
461
-
properties: {},
462
-
};
463
464
-
const required: string[] = [];
0
0
0
0
465
466
-
for (const [paramName, param] of operation.parameters.properties) {
467
-
const paramDef = this.typeToLexiconDefinition(param.type, param);
468
-
if (paramDef) {
469
-
params.properties[paramName] = paramDef;
470
-
if (!param.optional) {
471
-
required.push(paramName);
472
-
}
473
-
}
0
0
0
0
474
}
0
0
475
476
-
if (required.length > 0) {
477
-
params.required = required;
478
-
}
479
480
-
queryDef.parameters = params;
481
-
}
482
483
-
// Handle output
484
-
const customEncoding = getEncoding(this.program, operation);
485
-
if (operation.returnType && operation.returnType.kind !== "Intrinsic") {
486
-
const outputSchema = this.typeToLexiconDefinition(operation.returnType);
487
-
if (outputSchema) {
488
-
queryDef.output = {
489
-
encoding: customEncoding || "application/json",
490
-
schema: outputSchema,
491
-
};
492
-
}
493
-
} else if (customEncoding) {
494
-
// Custom encoding with no schema (e.g., application/jsonl with void return)
495
-
queryDef.output = {
496
-
encoding: customEncoding,
497
-
};
498
-
}
499
500
-
// Handle errors
501
-
const errors = getErrors(this.program, operation);
502
-
if (errors && errors.length > 0) {
503
-
queryDef.errors = errors;
504
-
}
505
506
-
lexicon.defs[defName] = queryDef;
507
-
} else if (isProcedure(this.program, operation)) {
508
-
// Build procedure definition
509
-
const procedureDef: any = {
510
-
type: "procedure",
511
-
};
512
513
-
if (description) {
514
-
procedureDef.description = description;
0
0
0
0
0
0
515
}
0
0
516
517
-
// Validate and parse operation parameters
518
-
if (operation.parameters && operation.parameters.properties.size > 0) {
519
-
const paramCount = operation.parameters.properties.size;
520
-
const params = Array.from(operation.parameters.properties) as [
521
-
string,
522
-
any,
523
-
][];
0
0
0
0
0
0
0
524
525
-
// Validate param count
526
-
if (paramCount > 2) {
0
0
527
this.program.reportDiagnostic({
528
-
code: "procedure-too-many-params",
529
severity: "error",
530
message:
531
-
"Procedures can have at most 2 parameters (input and/or parameters)",
532
-
target: operation,
533
});
534
-
} else if (paramCount === 1) {
535
-
// Single param: must be named "input"
536
-
const [paramName, param] = params[0] as [string, any];
0
537
538
-
if (paramName !== "input") {
539
-
this.program.reportDiagnostic({
540
-
code: "procedure-invalid-param-name",
541
-
severity: "error",
542
-
message: `Procedure parameter must be named "input", got "${paramName}"`,
543
-
target: param,
544
-
});
545
-
}
546
547
-
// Treat as input
548
-
const inputSchema = this.typeToLexiconDefinition(param.type);
549
-
if (inputSchema) {
550
-
const inputEncoding = getEncoding(this.program, param);
551
-
procedureDef.input = {
552
-
encoding: inputEncoding || "application/json",
553
-
schema: inputSchema,
554
-
};
555
-
}
556
-
} else if (paramCount === 2) {
557
-
// Two params: must be "input" and "parameters"
558
-
const [param1Name, param1] = params[0] as [string, any];
559
-
const [param2Name, param2] = params[1] as [string, any];
560
561
-
if (param1Name !== "input") {
562
-
this.program.reportDiagnostic({
563
-
code: "procedure-invalid-first-param",
564
-
severity: "error",
565
-
message: `First parameter must be named "input", got "${param1Name}"`,
566
-
target: param1,
567
-
});
568
-
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
569
570
-
if (param2Name !== "parameters") {
571
-
this.program.reportDiagnostic({
572
-
code: "procedure-invalid-second-param",
573
-
severity: "error",
574
-
message: `Second parameter must be named "parameters", got "${param2Name}"`,
575
-
target: param2,
576
-
});
577
-
}
578
579
-
// Validate that parameters is a plain object (not a model reference)
580
-
if (param2.type.kind !== "Model" || (param2.type as any).name) {
581
-
this.program.reportDiagnostic({
582
-
code: "procedure-parameters-not-object",
583
-
severity: "error",
584
-
message:
585
-
"The 'parameters' parameter must be a plain object, not a model reference",
586
-
target: param2,
587
-
});
588
-
}
589
590
-
// Handle input (first param)
591
-
const inputSchema = this.typeToLexiconDefinition(param1.type);
592
-
if (inputSchema) {
593
-
const inputEncoding = getEncoding(this.program, param1);
594
-
procedureDef.input = {
595
-
encoding: inputEncoding || "application/json",
596
-
schema: inputSchema,
597
-
};
598
-
}
599
600
-
// Handle parameters (second param)
601
-
const parametersModel = param2.type as any;
602
-
if (parametersModel.kind === "Model" && parametersModel.properties) {
603
-
const paramsObj: any = {
604
-
type: "params",
605
-
properties: {},
606
-
};
607
608
-
const required: string[] = [];
0
609
610
-
for (const [propName, prop] of parametersModel.properties) {
611
-
const propDef = this.typeToLexiconDefinition(prop.type, prop);
612
-
if (propDef) {
613
-
paramsObj.properties[propName] = propDef;
614
-
if (!prop.optional) {
615
-
required.push(propName);
616
-
}
617
-
}
618
-
}
619
620
-
if (required.length > 0) {
621
-
paramsObj.required = required;
622
-
}
0
0
0
0
0
0
623
624
-
procedureDef.parameters = paramsObj;
625
-
}
626
-
}
0
0
0
0
0
0
627
}
0
0
0
0
628
629
-
// Handle output (with custom encoding on operation)
630
-
const outputEncoding = getEncoding(this.program, operation);
631
-
if (operation.returnType && operation.returnType.kind !== "Intrinsic") {
632
-
const outputSchema = this.typeToLexiconDefinition(operation.returnType);
633
-
if (outputSchema) {
634
-
procedureDef.output = {
635
-
encoding: outputEncoding || "application/json",
636
-
schema: outputSchema,
637
-
};
638
-
}
639
-
} else if (outputEncoding) {
640
-
// Custom encoding with no schema (e.g., application/jsonl with void return)
641
-
procedureDef.output = {
642
-
encoding: outputEncoding,
643
-
};
644
}
645
646
-
// Handle errors
647
-
const errors = getErrors(this.program, operation);
648
-
if (errors && errors.length > 0) {
649
-
procedureDef.errors = errors;
0
0
0
650
}
651
652
-
lexicon.defs[defName] = procedureDef;
653
-
} else if (isSubscription(this.program, operation)) {
654
-
// Build subscription definition
655
-
const subscriptionDef: any = {
656
-
type: "subscription",
657
-
};
658
-
659
-
if (description) {
660
-
subscriptionDef.description = description;
661
}
662
663
-
// Handle parameters (same as query)
664
-
if (operation.parameters && operation.parameters.properties.size > 0) {
665
-
const params: any = {
666
-
type: "params",
667
-
properties: {},
668
-
};
669
0
0
0
670
const required: string[] = [];
671
672
-
for (const [paramName, param] of operation.parameters.properties) {
673
-
const paramDef = this.typeToLexiconDefinition(param.type, param);
674
-
if (paramDef) {
675
-
params.properties[paramName] = paramDef;
676
-
if (!param.optional) {
677
-
required.push(paramName);
678
-
}
679
}
680
}
681
682
-
if (required.length > 0) {
683
-
params.required = required;
684
-
}
685
-
686
-
subscriptionDef.parameters = params;
687
}
0
0
688
689
-
// Handle message (return type must be a union)
690
-
if (operation.returnType && operation.returnType.kind === "Union") {
691
-
const messageSchema = this.typeToLexiconDefinition(
692
-
operation.returnType,
693
-
);
694
-
if (messageSchema) {
695
-
subscriptionDef.message = {
696
-
schema: messageSchema,
697
-
};
698
-
}
699
-
} else if (
700
-
operation.returnType &&
701
-
operation.returnType.kind !== "Intrinsic"
702
-
) {
703
-
this.program.reportDiagnostic({
704
-
code: "subscription-return-not-union",
705
-
severity: "error",
706
-
message: "Subscription return type must be a union",
707
-
target: operation,
708
-
});
709
}
0
0
0
0
710
711
-
// Handle errors
712
-
const errors = getErrors(this.program, operation);
713
-
if (errors && errors.length > 0) {
714
-
subscriptionDef.errors = errors;
0
715
}
716
-
717
-
lexicon.defs[defName] = subscriptionDef;
0
0
0
0
0
718
}
0
0
0
0
0
719
}
720
721
private visitModel(model: Model) {
···
788
model: Model,
789
includeModelDescription: boolean = true,
790
): LexiconObject {
791
-
const description = includeModelDescription
792
-
? getDoc(this.program, model)
793
-
: undefined;
794
const required: string[] = [];
795
const nullable: string[] = [];
796
const properties: any = {};
797
798
for (const [name, prop] of model.properties) {
799
-
if (prop.optional !== true) {
800
-
// Field is required - check if it has the @required decorator
801
if (!isRequired(this.program, prop)) {
802
this.program.reportDiagnostic({
803
code: "closed-open-union-inline",
···
812
required.push(name);
813
}
814
815
-
// Check if property type is a union with null
816
let typeToProcess = prop.type;
817
if (prop.type.kind === "Union") {
818
-
const unionType = prop.type as Union;
819
-
const variants = Array.from(unionType.variants.values());
820
-
821
-
// Check if null is one of the variants
822
const hasNull = variants.some(
823
(v) => v.type.kind === "Intrinsic" && (v.type as any).name === "null",
824
);
825
826
if (hasNull) {
827
-
// Mark this property as nullable
828
nullable.push(name);
829
-
830
-
// Find the non-null variant
831
const nonNullVariant = variants.find(
832
-
(v) =>
833
-
!(v.type.kind === "Intrinsic" && (v.type as any).name === "null"),
834
);
835
-
if (nonNullVariant) {
836
-
typeToProcess = nonNullVariant.type;
837
-
}
838
}
839
}
840
841
const propDef = this.typeToLexiconDefinition(typeToProcess, prop);
842
-
if (propDef) {
843
-
properties[name] = propDef;
844
-
}
845
-
}
846
-
847
-
// Build object with correct key order
848
-
const obj: any = {
849
-
type: "object",
850
-
};
851
-
852
-
if (description) {
853
-
obj.description = description;
854
}
855
856
-
if (required.length > 0) {
857
-
obj.required = required;
858
-
}
859
-
860
-
if (nullable.length > 0) {
861
-
obj.nullable = nullable;
862
-
}
863
-
864
obj.properties = properties;
865
-
866
return obj;
867
}
868
···
871
prop?: ModelProperty,
872
isDefining?: boolean,
873
): LexiconDefinition | null {
0
0
874
switch (type.kind) {
875
case "Namespace": {
876
-
// Handle namespace references - look for Main model in the namespace
877
-
const namespace = type as any;
878
-
const mainModel = namespace.models?.get("Main");
879
if (mainModel) {
880
const ref = this.getModelReference(mainModel);
881
-
if (ref) {
882
-
const refDef: LexiconRef = {
883
-
type: "ref",
884
-
ref: ref,
885
-
};
886
-
if (prop) {
887
-
const propDesc = getDoc(this.program, prop);
888
-
if (propDesc) {
889
-
refDef.description = propDesc;
890
-
}
891
-
}
892
-
return refDef;
893
-
}
894
}
895
return null;
896
}
897
case "Enum": {
898
-
// Handle enum types - convert to primitive with enum constraint
899
-
const enumType = type as any;
900
-
const members = Array.from(enumType.members?.values?.() || []);
901
const values = members.map((m: any) => m.value);
902
-
903
-
// Determine if this is a string or integer enum
904
const firstValue = values[0];
905
-
const isStringEnum = typeof firstValue === "string";
906
-
const isIntegerEnum =
907
-
typeof firstValue === "number" && Number.isInteger(firstValue);
908
909
-
if (isStringEnum) {
910
-
const primitive: LexiconPrimitive = {
911
-
type: "string",
912
-
enum: values as string[],
913
-
};
914
-
if (prop) {
915
-
const propDesc = getDoc(this.program, prop);
916
-
if (propDesc) {
917
-
primitive.description = propDesc;
918
-
}
919
-
}
920
-
return primitive;
921
-
} else if (isIntegerEnum) {
922
-
const primitive: LexiconPrimitive = {
923
-
type: "integer",
924
-
enum: values as number[],
925
-
};
926
-
if (prop) {
927
-
const propDesc = getDoc(this.program, prop);
928
-
if (propDesc) {
929
-
primitive.description = propDesc;
930
-
}
931
-
}
932
-
return primitive;
933
}
934
-
// Unsupported enum type (e.g., float)
935
return null;
936
}
937
case "Boolean": {
938
-
// Handle boolean literal types (e.g., `true` or `false`)
939
-
const booleanType = type as any;
940
-
const primitive: LexiconPrimitive = {
941
type: "boolean",
942
-
const: booleanType.value,
943
-
};
944
-
if (prop) {
945
-
const propDesc = getDoc(this.program, prop);
946
-
if (propDesc) {
947
-
primitive.description = propDesc;
948
-
}
949
-
}
950
-
return primitive;
951
}
952
case "Scalar":
953
const scalar = type as Scalar;
954
const primitive = this.scalarToLexiconPrimitive(scalar, prop);
955
-
if (primitive) {
956
-
// Property description takes precedence over scalar description
957
-
if (prop) {
958
-
const propDesc = getDoc(this.program, prop);
959
-
if (propDesc) {
960
-
primitive.description = propDesc;
961
-
}
962
-
}
963
-
// If no property description, use scalar's description for user-defined scalars
964
-
// Exclude: TypeSpec built-ins and our predefined format scalars
965
-
if (
966
-
!primitive.description &&
967
-
scalar.baseScalar &&
968
-
scalar.namespace?.name !== "TypeSpec"
969
-
) {
970
-
// List of predefined format scalars (from @tlex/emitter and TypeSpec)
971
-
const FORMAT_SCALARS = new Set([
972
-
"datetime",
973
-
"did",
974
-
"handle",
975
-
"atUri",
976
-
"cid",
977
-
"tid",
978
-
"nsid",
979
-
"recordKey",
980
-
"uri",
981
-
"language",
982
-
"atIdentifier",
983
-
"bytes",
984
-
"utcDateTime",
985
-
"offsetDateTime",
986
-
"plainDate",
987
-
"plainTime",
988
-
]);
989
990
-
if (!FORMAT_SCALARS.has(scalar.name)) {
991
-
const scalarDesc = getDoc(this.program, scalar);
992
-
if (scalarDesc) {
993
-
primitive.description = scalarDesc;
994
-
}
995
-
}
0
0
0
0
0
996
}
997
}
998
return primitive;
999
case "Model":
1000
-
// Check if this is the Blob model or extends Blob
1001
const model = type as Model;
1002
1003
-
// Check if this is a Blob model instance
1004
-
// Strategy: Check decorator first (most reliable), then template instance pattern, then extends
1005
const isBlobModel =
1006
isBlob(this.program, model) ||
1007
(isTemplateInstance(model) && model.templateNode && isBlob(this.program, model.templateNode as any)) ||
1008
(model.baseModel && isBlob(this.program, model.baseModel));
1009
1010
if (isBlobModel) {
1011
-
// Extract template parameters if this is a template instance
1012
-
let acceptTypes: string[] | undefined;
1013
-
let maxSize: number | undefined;
1014
-
1015
-
if (isTemplateInstance(model)) {
1016
-
const templateArgs = model.templateMapper?.args;
1017
-
if (templateArgs && templateArgs.length >= 2) {
1018
-
// First arg is Accept (valueof unknown - tuple)
1019
-
const acceptArg = templateArgs[0] as any;
1020
-
// Handle both Type and Value cases
1021
-
if (acceptArg && acceptArg.type?.kind === "Tuple") {
1022
-
const tuple = acceptArg.type;
1023
-
if (tuple.values && tuple.values.length > 0) {
1024
-
// Extract string values from the tuple
1025
-
acceptTypes = tuple.values
1026
-
.map((v: any) => (v.kind === "String" ? v.value : null))
1027
-
.filter((v: string | null) => v !== null) as string[];
1028
-
if (acceptTypes.length === 0) acceptTypes = undefined;
1029
-
}
1030
-
} else if (acceptArg && Array.isArray(acceptArg.value)) {
1031
-
// Handle direct array values
1032
-
const values = acceptArg.value.filter(
1033
-
(v: any) => typeof v === "string",
1034
-
);
1035
-
if (values.length > 0) acceptTypes = values;
1036
-
}
1037
-
1038
-
// Second arg is MaxSize (valueof int32)
1039
-
const maxSizeArg = templateArgs[1] as any;
1040
-
if (maxSizeArg && typeof maxSizeArg.value === "number") {
1041
-
maxSize = maxSizeArg.value;
1042
-
} else if (maxSizeArg && maxSizeArg.type?.kind === "Number") {
1043
-
maxSize = Number(maxSizeArg.type.value);
1044
-
}
1045
-
}
1046
-
}
1047
-
// Always emit as blob type (with or without constraints)
1048
-
const blobDef: LexiconBlob = {
1049
-
type: "blob",
1050
-
};
1051
-
if (acceptTypes) {
1052
-
blobDef.accept = acceptTypes;
1053
-
}
1054
-
if (maxSize !== undefined && maxSize !== 0) {
1055
-
blobDef.maxSize = maxSize;
1056
-
}
1057
-
if (prop) {
1058
-
const propDesc = getDoc(this.program, prop);
1059
-
if (propDesc) {
1060
-
(blobDef as any).description = propDesc;
1061
-
}
1062
-
}
1063
-
return blobDef;
1064
}
1065
1066
-
// Check if this is a Closed<T> model instance
1067
const isClosedModel =
1068
model.name === "Closed" ||
1069
(model.node && (model.node as any).symbol?.name === "Closed") ||
1070
-
(isTemplateInstance(model) &&
1071
-
model.node &&
1072
-
(model.node as any).symbol?.name === "Closed");
1073
1074
if (isClosedModel && isTemplateInstance(model)) {
1075
-
// Extract the union type parameter
1076
-
const templateArgs = model.templateMapper?.args;
1077
-
if (templateArgs && templateArgs.length > 0) {
1078
-
const unionArg = templateArgs[0];
1079
-
if (isType(unionArg) && unionArg.kind === "Union") {
1080
-
// Process the union with closed flag
1081
-
const unionDef = this.typeToLexiconDefinition(unionArg, prop);
1082
-
if (unionDef && unionDef.type === "union") {
1083
-
(unionDef as LexiconUnion).closed = true;
1084
-
return unionDef;
1085
-
}
1086
}
1087
}
1088
}
1089
1090
-
// Check if this is a reference to another model (including named array models)
1091
-
// This must come BEFORE the array check to ensure named arrays are referenced, not inlined
1092
-
const modelRef = this.getModelReference(type as Model);
1093
if (modelRef) {
1094
-
const refDef: LexiconRef = {
1095
-
type: "ref",
1096
-
ref: modelRef,
1097
-
};
1098
-
if (prop) {
1099
-
const propDesc = getDoc(this.program, prop);
1100
-
if (propDesc) {
1101
-
refDef.description = propDesc;
1102
-
}
1103
-
}
1104
-
return refDef;
1105
}
1106
1107
-
// Check for anonymous array types (inline arrays like `SomeModel[]` in property)
1108
-
if (isArrayModelType(this.program, type as Model)) {
1109
-
const array = this.modelToLexiconArray(type as Model, prop);
1110
-
if (array && prop) {
1111
-
const propDesc = getDoc(this.program, prop);
1112
-
if (propDesc) {
1113
-
array.description = propDesc;
1114
-
}
1115
-
}
1116
-
return array;
1117
-
}
1118
-
const obj = this.modelToLexiconObject(type as Model);
1119
-
if (prop) {
1120
-
const propDesc = getDoc(this.program, prop);
1121
-
if (propDesc) {
1122
-
obj.description = propDesc;
1123
-
}
1124
}
1125
-
return obj;
0
1126
case "Union":
1127
-
// Handle union types naturally
1128
const unionType = type as Union;
1129
1130
-
// Check if this is a named union that should be referenced
1131
-
// (but not if we're defining the union itself)
1132
if (!isDefining) {
1133
const unionRef = this.getUnionReference(unionType);
1134
if (unionRef) {
1135
-
const refDef: LexiconRef = {
1136
-
type: "ref",
1137
-
ref: unionRef,
1138
-
};
1139
-
if (prop) {
1140
-
const propDesc = getDoc(this.program, prop);
1141
-
if (propDesc) {
1142
-
refDef.description = propDesc;
1143
-
}
1144
-
}
1145
-
return refDef;
1146
-
}
1147
-
}
1148
-
1149
-
const unionRefs: string[] = [];
1150
-
const stringLiterals: string[] = [];
1151
-
let hasStringType = false;
1152
-
let hasUnknown = false;
1153
-
1154
-
// Iterate through all variants in the union
1155
-
for (const variant of unionType.variants.values()) {
1156
-
if (variant.type.kind === "Model") {
1157
-
const ref = this.getModelReference(variant.type as Model);
1158
-
if (ref) {
1159
-
unionRefs.push(ref);
1160
-
}
1161
-
} else if (variant.type.kind === "String") {
1162
-
// String literal
1163
-
stringLiterals.push((variant.type as any).value);
1164
-
} else if (
1165
-
variant.type.kind === "Scalar" &&
1166
-
(variant.type as Scalar).name === "string"
1167
-
) {
1168
-
// String type
1169
-
hasStringType = true;
1170
-
} else if (variant.type.kind === "Intrinsic") {
1171
-
// Check for unknown or never (both indicate open union)
1172
-
const intrinsicName = (variant.type as any).name;
1173
-
if (intrinsicName === "unknown" || intrinsicName === "never") {
1174
-
hasUnknown = true;
1175
-
}
1176
}
1177
}
1178
1179
-
// If union is string literals + string, emit as knownValues
1180
-
if (
1181
-
stringLiterals.length > 0 &&
1182
-
hasStringType &&
1183
-
unionRefs.length === 0
1184
-
) {
1185
-
const primitive: LexiconPrimitive = {
1186
-
type: "string",
1187
-
knownValues: stringLiterals,
1188
-
};
1189
-
1190
-
// Add decorators from the union itself (for def-level constraints)
1191
-
const maxLength = getMaxLength(this.program, unionType);
1192
-
if (maxLength !== undefined) {
1193
-
primitive.maxLength = maxLength;
1194
-
}
1195
-
1196
-
const minLength = getMinLength(this.program, unionType);
1197
-
if (minLength !== undefined) {
1198
-
primitive.minLength = minLength;
1199
-
}
1200
-
1201
-
const maxGraphemes = getMaxGraphemes(this.program, unionType);
1202
-
if (maxGraphemes !== undefined) {
1203
-
primitive.maxGraphemes = maxGraphemes;
1204
-
}
1205
-
1206
-
const minGraphemes = getMinGraphemes(this.program, unionType);
1207
-
if (minGraphemes !== undefined) {
1208
-
primitive.minGraphemes = minGraphemes;
1209
-
}
1210
-
1211
-
if (prop) {
1212
-
const propDesc = getDoc(this.program, prop);
1213
-
if (propDesc) {
1214
-
primitive.description = propDesc;
1215
-
}
1216
-
// Check for default values
1217
-
const defaultValue = (prop as any).default;
1218
-
if (defaultValue && (defaultValue as any).value !== undefined) {
1219
-
const value = (defaultValue as any).value;
1220
-
if (typeof value === "string") {
1221
-
(primitive as any).default = value;
1222
-
}
1223
-
}
1224
-
}
1225
-
return primitive;
1226
-
}
1227
-
1228
-
// Otherwise, emit as union of refs
1229
-
if (unionRefs.length > 0) {
1230
-
// Check for unsupported mixed pattern: model refs + string literals
1231
-
if (stringLiterals.length > 0) {
1232
-
this.program.reportDiagnostic({
1233
-
code: "union-mixed-refs-literals",
1234
-
severity: "error",
1235
-
message:
1236
-
`Union contains both model references and string literals. Atproto unions must be either: ` +
1237
-
`(1) model references only (type: "union"), or ` +
1238
-
`(2) string literals + string type (type: "string" with knownValues). ` +
1239
-
`Separate these into distinct fields or nested unions.`,
1240
-
target: unionType,
1241
-
});
1242
-
return null;
1243
-
}
1244
-
1245
-
const unionDef: LexiconUnion = {
1246
-
type: "union",
1247
-
refs: unionRefs,
1248
-
};
1249
-
1250
-
// Check for @closed decorator on the union itself (not on properties)
1251
-
if (isClosed(this.program, unionType)) {
1252
-
// Validate that @closed is not used on open unions (with unknown/never)
1253
-
if (hasUnknown) {
1254
-
this.program.reportDiagnostic({
1255
-
code: "closed-open-union",
1256
-
severity: "error",
1257
-
message:
1258
-
"@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.",
1259
-
target: unionType,
1260
-
});
1261
-
} else {
1262
-
unionDef.closed = true;
1263
-
}
1264
-
}
1265
-
1266
-
if (prop) {
1267
-
const propDesc = getDoc(this.program, prop);
1268
-
if (propDesc) {
1269
-
unionDef.description = propDesc;
1270
-
}
1271
-
}
1272
-
return unionDef;
1273
-
}
1274
-
1275
-
// No refs and no string literals - empty or unsupported union
1276
-
if (stringLiterals.length === 0 && !hasUnknown) {
1277
-
this.program.reportDiagnostic({
1278
-
code: "union-empty",
1279
-
severity: "error",
1280
-
message:
1281
-
`Union has no variants. Atproto unions must contain either model references or string literals.`,
1282
-
target: unionType,
1283
-
});
1284
-
}
1285
-
1286
-
return null;
1287
case "Intrinsic":
1288
-
// Handle unknown type - return unknown definition
1289
-
const unknownDef: any = {
1290
-
type: "unknown",
1291
-
};
1292
-
if (prop) {
1293
-
const propDesc = getDoc(this.program, prop);
1294
-
if (propDesc) {
1295
-
unknownDef.description = propDesc;
1296
-
}
1297
-
}
1298
-
return unknownDef;
1299
default:
1300
return null;
1301
}
···
1305
scalar: Scalar,
1306
prop?: ModelProperty,
1307
): LexiconDefinition | null {
1308
-
const primitive: LexiconPrimitive = {
1309
-
type: "string", // default
0
0
0
0
1310
};
1311
1312
-
switch (scalar.name) {
1313
-
case "string":
1314
-
primitive.type = "string";
1315
-
break;
1316
-
case "boolean":
1317
-
primitive.type = "boolean";
1318
-
break;
1319
-
case "integer":
1320
-
case "int32":
1321
-
case "int64":
1322
-
case "int16":
1323
-
case "int8":
1324
-
primitive.type = "integer";
1325
-
break;
1326
-
case "float32":
1327
-
case "float64":
1328
-
primitive.type = "number";
1329
-
break;
1330
-
case "utcDateTime":
1331
-
case "offsetDateTime":
1332
-
case "plainDate":
1333
-
case "plainTime":
1334
-
primitive.type = "string";
1335
-
primitive.format = "datetime";
1336
-
break;
1337
-
// Pre-defined format scalars from @tlex/emitter
1338
-
case "did":
1339
-
primitive.type = "string";
1340
-
primitive.format = "did";
1341
-
break;
1342
-
case "handle":
1343
-
primitive.type = "string";
1344
-
primitive.format = "handle";
1345
-
break;
1346
-
case "atUri":
1347
-
primitive.type = "string";
1348
-
primitive.format = "at-uri";
1349
-
break;
1350
-
case "datetime":
1351
-
primitive.type = "string";
1352
-
primitive.format = "datetime";
1353
-
break;
1354
-
case "cid":
1355
-
primitive.type = "string";
1356
-
primitive.format = "cid";
1357
-
break;
1358
-
case "tid":
1359
-
primitive.type = "string";
1360
-
primitive.format = "tid";
1361
-
break;
1362
-
case "nsid":
1363
-
primitive.type = "string";
1364
-
primitive.format = "nsid";
1365
-
break;
1366
-
case "recordKey":
1367
-
primitive.type = "string";
1368
-
primitive.format = "record-key";
1369
-
break;
1370
-
case "uri":
1371
-
primitive.type = "string";
1372
-
primitive.format = "uri";
1373
-
break;
1374
-
case "language":
1375
-
primitive.type = "string";
1376
-
primitive.format = "language";
1377
-
break;
1378
-
case "atIdentifier":
1379
-
primitive.type = "string";
1380
-
primitive.format = "at-identifier";
1381
-
break;
1382
-
case "bytes":
1383
-
// Check if this has blob-specific decorators
1384
-
if (prop) {
1385
-
const accept = getBlobAccept(this.program, prop);
1386
-
const maxSize = getBlobMaxSize(this.program, prop);
1387
-
1388
-
// If it has blob decorators, emit as blob type
1389
-
if (accept || maxSize !== undefined) {
1390
-
const blobDef: LexiconBlob = {
1391
-
type: "blob",
1392
-
};
1393
-
if (accept) {
1394
-
blobDef.accept = accept;
1395
-
}
1396
-
if (maxSize !== undefined) {
1397
-
blobDef.maxSize = maxSize;
1398
-
}
1399
-
return blobDef;
1400
-
}
1401
}
1402
-
1403
-
// Otherwise, emit as bytes primitive
1404
-
const bytesDef: LexiconBytes = {
1405
-
type: "bytes",
1406
-
};
1407
-
return bytesDef;
1408
}
1409
1410
-
// Check for decorators on the property or scalar
1411
-
const target = prop || scalar;
0
0
0
1412
1413
-
const format = getLexFormat(this.program, target);
1414
-
if (format) {
1415
-
primitive.format = format;
1416
-
}
1417
0
1418
const maxLength = getMaxLength(this.program, target);
1419
-
if (maxLength !== undefined) {
1420
-
primitive.maxLength = maxLength;
1421
-
}
1422
-
1423
const minLength = getMinLength(this.program, target);
1424
-
if (minLength !== undefined) {
1425
-
primitive.minLength = minLength;
1426
-
}
1427
-
1428
const maxGraphemes = getMaxGraphemes(this.program, target);
1429
-
if (maxGraphemes !== undefined) {
1430
-
primitive.maxGraphemes = maxGraphemes;
1431
-
}
1432
-
1433
const minGraphemes = getMinGraphemes(this.program, target);
1434
-
if (minGraphemes !== undefined) {
1435
-
primitive.minGraphemes = minGraphemes;
1436
-
}
1437
1438
-
// The rest of the decorators only apply to properties
1439
if (prop) {
1440
-
// Check for const value on boolean, string, or integer properties
1441
const constValue = getLexConst(this.program, prop);
1442
-
if (constValue !== undefined) {
1443
-
if (primitive.type === "boolean" && typeof constValue === "boolean") {
1444
-
(primitive as any).const = constValue;
1445
-
} else if (
1446
-
primitive.type === "string" &&
1447
-
typeof constValue === "string"
1448
-
) {
1449
-
(primitive as any).const = constValue;
1450
-
} else if (
1451
-
primitive.type === "integer" &&
1452
-
typeof constValue === "number"
1453
-
) {
1454
-
(primitive as any).const = constValue;
1455
-
}
1456
}
1457
1458
-
// Check for default values (supported on string, integer, boolean)
1459
-
const defaultValue = (prop as any).default;
1460
-
if (defaultValue && (defaultValue as any).value !== undefined) {
1461
-
const value = (defaultValue as any).value;
1462
-
if (primitive.type === "string" && typeof value === "string") {
1463
-
(primitive as any).default = value;
1464
-
} else if (primitive.type === "integer" && typeof value === "number") {
1465
-
(primitive as any).default = value;
1466
-
} else if (primitive.type === "boolean" && typeof value === "boolean") {
1467
-
(primitive as any).default = value;
1468
-
}
1469
}
1470
-
}
1471
1472
-
// Add minimum constraint for integer/number types
1473
-
if (prop && (primitive.type === "integer" || primitive.type === "number")) {
1474
-
const minValue = getMinValue(this.program, prop);
1475
-
if (minValue !== undefined) {
1476
-
primitive.minimum = minValue;
1477
-
}
1478
-
const maxValue = getMaxValue(this.program, prop);
1479
-
if (maxValue !== undefined) {
1480
-
primitive.maximum = maxValue;
1481
}
1482
}
1483
···
1485
}
1486
1487
private getModelReference(model: Model): string | null {
1488
-
// Refs can be local (#defName) or global (nsid#defName)
1489
-
// Per atproto spec: local refs for same lexicon, global refs for cross-lexicon
1490
-
1491
-
// Anonymous/inline models don't have refs
1492
-
if (!model.name || model.name === "") {
1493
-
return null;
1494
-
}
1495
-
1496
-
if (
1497
-
!model.namespace ||
1498
-
model.namespace.name === "" ||
1499
-
model.namespace.name === "TypeSpec"
1500
-
) {
1501
-
return null;
1502
-
}
1503
1504
const namespaceName = getNamespaceFullName(model.namespace);
1505
-
if (!namespaceName) {
1506
-
return null;
1507
-
}
1508
1509
const defName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
1510
1511
-
// Check if it's in the current lexicon being processed (use local ref)
1512
-
if (this.currentLexiconId) {
1513
-
// Check for exact match (Main model case: lexicon ID = namespace)
1514
-
if (this.currentLexiconId === namespaceName) {
1515
-
return `#${defName}`;
1516
-
}
1517
-
// Check for defs file match
1518
-
if (this.currentLexiconId === `${namespaceName}.defs`) {
1519
-
return `#${defName}`;
1520
-
}
1521
}
1522
1523
-
// Different lexicon - use global ref
1524
-
// If the model is named "Main", it's the main def (no #main suffix per spec)
1525
-
if (model.name === "Main") {
1526
-
return namespaceName;
1527
-
}
1528
-
1529
-
// All other defs use fragment syntax
1530
-
return `${namespaceName}#${defName}`;
1531
}
1532
1533
private getUnionReference(union: Union): string | null {
1534
-
// Check if this union has a name (is a named def)
1535
const unionName = (union as any).name;
1536
-
if (!unionName) {
1537
-
return null;
1538
-
}
1539
-
1540
-
// Check if union has a namespace
1541
const namespace = (union as any).namespace;
1542
-
if (!namespace || namespace.name === "" || namespace.name === "TypeSpec") {
1543
-
return null;
1544
-
}
1545
1546
const namespaceName = getNamespaceFullName(namespace);
1547
-
if (!namespaceName) {
1548
-
return null;
1549
-
}
1550
1551
const defName = unionName.charAt(0).toLowerCase() + unionName.slice(1);
1552
1553
-
// Check if it's in the current lexicon being processed (use local ref)
1554
-
if (this.currentLexiconId) {
1555
-
// Check for exact match
1556
-
if (this.currentLexiconId === namespaceName) {
1557
-
return `#${defName}`;
1558
-
}
1559
-
// Check for defs file match
1560
-
if (this.currentLexiconId === `${namespaceName}.defs`) {
1561
-
return `#${defName}`;
1562
-
}
1563
}
1564
1565
-
// Different lexicon - use global ref with fragment syntax
1566
return `${namespaceName}#${defName}`;
1567
}
1568
···
1570
model: Model,
1571
prop?: ModelProperty,
1572
): LexiconArray | null {
1573
-
// For `is` arrays (e.g., `model Preferences is SomeUnion[]`),
1574
-
// the template args are on the sourceModel, not the model itself
1575
const arrayModel = model.sourceModel || model;
0
1576
1577
-
// Handle TypeSpec array types
1578
-
if (arrayModel.templateMapper?.args && arrayModel.templateMapper.args.length > 0) {
1579
-
const itemType = arrayModel.templateMapper.args[0];
1580
1581
-
if (isType(itemType)) {
1582
-
const itemDef = this.typeToLexiconDefinition(itemType);
1583
1584
-
if (itemDef) {
1585
-
const arrayDef: LexiconArray = {
1586
-
type: "array",
1587
-
items: itemDef,
1588
-
};
1589
-
1590
-
// Add array constraints from property decorators
1591
-
if (prop) {
1592
-
const maxItems = getMaxItems(this.program, prop);
1593
-
if (maxItems !== undefined) {
1594
-
arrayDef.maxLength = maxItems;
1595
-
}
1596
1597
-
const minItems = getMinItems(this.program, prop);
1598
-
if (minItems !== undefined) {
1599
-
arrayDef.minLength = minItems;
1600
-
}
1601
-
}
1602
-
1603
-
return arrayDef;
1604
-
}
1605
-
}
1606
}
1607
1608
return null;
1609
}
1610
1611
private getModelLexiconId(model: Model): string | null {
1612
-
if (!model.namespace || model.namespace.name === "") {
1613
-
return null;
1614
-
}
1615
1616
const namespaceName = getNamespaceFullName(model.namespace);
1617
-
if (!namespaceName) {
1618
-
return null;
1619
-
}
1620
1621
-
// If the model is named "Main", the lexicon ID is just the namespace
1622
-
// Otherwise, append the lowercased model name
1623
-
if (model.name === "Main") {
1624
-
return namespaceName;
1625
-
}
1626
1627
-
// Convert namespace to lexicon ID format (e.g., "xyz.statusosphere" -> "xyz.statusosphere.modelName")
1628
return `${namespaceName}.${model.name.charAt(0).toLowerCase() + model.name.slice(1)}`;
1629
}
1630
1631
private getLexiconPath(lexiconId: string): string {
1632
-
// Convert lexicon ID to file path (e.g., "xyz.statusphere.status" -> "xyz/statusphere/status.json")
1633
const parts = lexiconId.split(".");
1634
-
const fileName = parts[parts.length - 1] + ".json";
1635
-
const dirs = parts.slice(0, -1);
1636
-
1637
-
return join(this.options.outputDir, ...dirs, fileName);
1638
}
1639
1640
private async writeFile(filePath: string, content: string) {
···
78
private processNamespace(ns: any) {
79
const fullName = getNamespaceFullName(ns);
80
0
81
if (fullName && !fullName.startsWith("TypeSpec")) {
0
82
const hasModels = ns.models.size > 0;
83
const hasScalars = ns.scalars.size > 0;
84
const hasUnions = ns.unions?.size > 0;
85
const hasOperations = ns.operations?.size > 0;
86
const hasChildNamespaces = ns.namespaces.size > 0;
87
+
const hasContent = hasModels || hasScalars || hasUnions;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
88
89
+
if (hasOperations) {
90
+
this.emitOperationLexicon(ns, fullName);
91
+
} else if (hasContent && !hasChildNamespaces) {
92
+
this.emitContentLexicon(ns, fullName);
93
+
} else if (hasContent && hasChildNamespaces) {
94
+
this.emitDefsLexicon(ns, fullName);
95
+
}
96
+
}
0
0
0
97
98
+
for (const [_, childNs] of ns.namespaces) {
99
+
this.processNamespace(childNs);
100
+
}
101
+
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
102
103
+
private emitContentLexicon(ns: any, fullName: string) {
104
+
const models = [...ns.models.values()];
105
+
const isDefsFile = fullName.endsWith(".defs");
106
+
const mainModel = isDefsFile ? null : models.find((m) => m.name === "Main");
0
107
108
+
if (!isDefsFile && !mainModel) {
109
+
throw new Error(
110
+
`Namespace "${fullName}" has models/scalars but no Main model. ` +
111
+
`Either add a "model Main" or rename namespace to "${fullName}.defs" for shared definitions.`,
112
+
);
113
+
}
114
115
+
this.currentLexiconId = fullName;
116
+
const lexicon = this.createLexicon(fullName, ns);
0
117
118
+
if (mainModel) {
119
+
lexicon.defs.main = this.createMainDef(mainModel);
120
+
this.addDefs(lexicon, ns, models.filter((m) => m.name !== "Main"));
121
+
} else {
122
+
this.addDefs(lexicon, ns, models);
123
+
}
124
125
+
this.lexicons.set(fullName, lexicon);
126
+
this.currentLexiconId = null;
127
+
}
0
0
128
129
+
private emitDefsLexicon(ns: any, fullName: string) {
130
+
const lexiconId = fullName.endsWith(".defs") ? fullName : fullName + ".defs";
131
+
this.currentLexiconId = lexiconId;
132
+
const lexicon = this.createLexicon(lexiconId, ns);
133
+
this.addDefs(lexicon, ns, [...ns.models.values()]);
134
+
this.lexicons.set(lexiconId, lexicon);
135
+
this.currentLexiconId = null;
136
+
}
0
0
137
138
+
private emitOperationLexicon(ns: any, fullName: string) {
139
+
this.currentLexiconId = fullName;
140
+
const lexicon = this.createLexicon(fullName, ns);
141
142
+
const mainOp = [...ns.operations].find(
143
+
([name]) => name === "main" || name === "Main"
144
+
)?.[1];
0
0
0
0
0
145
146
+
if (mainOp) {
147
+
this.addOperationToDefs(lexicon, mainOp, "main");
148
+
}
149
150
+
for (const [name, operation] of ns.operations) {
151
+
if (name !== "main" && name !== "Main") {
152
+
this.addOperationToDefs(lexicon, operation, name);
153
+
}
154
+
}
0
155
156
+
this.addDefs(lexicon, ns, [...ns.models.values()].filter((m) => m.name !== "Main"));
157
+
this.lexicons.set(fullName, lexicon);
158
+
this.currentLexiconId = null;
159
+
}
0
0
160
161
+
private createLexicon(id: string, ns: any): LexiconDocument {
162
+
const lexicon: LexiconDocument = { lexicon: 1, id, defs: {} };
163
+
const description = getDoc(this.program, ns);
164
+
if (description) lexicon.description = description;
165
+
return lexicon;
166
+
}
167
168
+
private createMainDef(mainModel: Model): any {
169
+
const modelDescription = getDoc(this.program, mainModel);
170
+
const recordKey = getRecordKey(this.program, mainModel);
171
+
const modelDef = this.modelToLexiconObject(mainModel, !!modelDescription);
0
0
172
173
+
if (recordKey) {
174
+
const recordDef: any = { type: "record", key: recordKey, record: modelDef };
175
+
if (modelDescription) {
176
+
recordDef.description = modelDescription;
177
+
delete modelDef.description;
178
}
179
+
return recordDef;
180
}
181
182
+
return modelDef;
183
+
}
184
+
185
+
private addDefs(lexicon: LexiconDocument, ns: any, models: Model[]) {
186
+
for (const model of models) {
187
+
this.addModelToDefs(lexicon, model);
188
+
}
189
+
for (const [_, scalar] of ns.scalars) {
190
+
this.addScalarToDefs(lexicon, scalar);
191
+
}
192
+
if (ns.unions) {
193
+
for (const [_, union] of ns.unions) {
194
+
this.addUnionToDefs(lexicon, union);
195
+
}
196
}
197
}
198
0
0
0
199
private addModelToDefs(lexicon: LexiconDocument, model: Model) {
200
+
if (model.name[0] !== model.name[0].toUpperCase()) {
0
201
this.program.reportDiagnostic({
202
code: "invalid-model-name",
203
severity: "error",
···
207
return;
208
}
209
210
+
if (isErrorModel(this.program, model)) return;
211
212
+
const defName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
213
+
const description = getDoc(this.program, model);
0
0
214
0
215
if (isToken(this.program, model)) {
216
+
lexicon.defs[defName] = this.addDescription({ type: "token" }, description);
0
0
0
0
0
0
0
0
0
217
return;
218
}
219
0
0
220
if (isArrayModelType(this.program, model)) {
221
const arrayDef = this.modelToLexiconArray(model);
222
if (arrayDef) {
223
+
lexicon.defs[defName] = this.addDescription(arrayDef, description);
0
0
0
0
224
return;
225
}
226
}
227
228
const modelDef = this.modelToLexiconObject(model);
229
+
lexicon.defs[defName] = this.addDescription(modelDef, description);
0
0
0
0
0
0
230
}
231
0
0
0
232
private addScalarToDefs(lexicon: LexiconDocument, scalar: Scalar) {
233
+
if (scalar.namespace?.name === "TypeSpec") return;
234
+
if (scalar.baseScalar?.namespace?.name === "TypeSpec") return;
0
0
0
0
0
0
0
0
0
0
0
235
236
const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1);
237
+
const scalarDef = this.scalarToLexiconPrimitive(scalar, undefined);
0
0
0
238
const description = getDoc(this.program, scalar);
239
+
lexicon.defs[defName] = this.addDescription(scalarDef, description);
0
0
0
0
240
}
241
0
0
0
242
private addUnionToDefs(lexicon: LexiconDocument, union: Union) {
243
+
const name = (union as any).name;
244
+
if (!name) return;
0
0
245
0
246
const unionDef: any = this.typeToLexiconDefinition(union, undefined, true);
247
if (!unionDef) return;
248
249
+
if (unionDef.type === "union" || (unionDef.type === "string" && unionDef.knownValues)) {
250
+
const defName = name.charAt(0).toLowerCase() + name.slice(1);
251
+
const description = getDoc(this.program, union);
252
+
lexicon.defs[defName] = this.addDescription(unionDef, description);
253
+
}
254
+
}
255
256
+
private addDescription(obj: any, description?: string): any {
257
+
if (description && !obj.description) {
258
+
obj.description = description;
0
0
0
0
0
259
}
260
+
return obj;
261
}
262
263
+
private createBlobDef(model: Model): LexiconBlob {
264
+
const blobDef: LexiconBlob = { type: "blob" };
265
+
266
+
if (isTemplateInstance(model)) {
267
+
const templateArgs = model.templateMapper?.args;
268
+
if (templateArgs?.length >= 2) {
269
+
const acceptArg = templateArgs[0] as any;
270
+
let acceptTypes: string[] | undefined;
271
+
272
+
if (acceptArg?.type?.kind === "Tuple") {
273
+
const tuple = acceptArg.type;
274
+
if (tuple.values?.length > 0) {
275
+
acceptTypes = tuple.values
276
+
.map((v: any) => (v.kind === "String" ? v.value : null))
277
+
.filter((v: string | null) => v !== null) as string[];
278
+
if (!acceptTypes.length) acceptTypes = undefined;
279
+
}
280
+
} else if (acceptArg && Array.isArray(acceptArg.value)) {
281
+
const values = acceptArg.value.filter((v: any) => typeof v === "string");
282
+
if (values.length) acceptTypes = values;
283
+
}
284
285
+
if (acceptTypes) blobDef.accept = acceptTypes;
0
0
0
0
286
287
+
const maxSizeArg = templateArgs[1] as any;
288
+
const maxSize = maxSizeArg?.value ?? (maxSizeArg?.type?.kind === "Number" ? Number(maxSizeArg.type.value) : undefined);
289
+
if (maxSize !== undefined && maxSize !== 0) blobDef.maxSize = maxSize;
290
}
291
+
}
292
293
+
return blobDef;
294
+
}
0
0
0
0
0
0
0
0
295
296
+
private processUnion(unionType: Union, prop?: ModelProperty): LexiconDefinition | null {
297
+
const unionRefs: string[] = [];
298
+
const stringLiterals: string[] = [];
299
+
let hasStringType = false;
300
+
let hasUnknown = false;
301
302
+
for (const variant of unionType.variants.values()) {
303
+
if (variant.type.kind === "Model") {
304
+
const ref = this.getModelReference(variant.type as Model);
305
+
if (ref) unionRefs.push(ref);
306
+
} else if (variant.type.kind === "String") {
307
+
stringLiterals.push((variant.type as any).value);
308
+
} else if (variant.type.kind === "Scalar" && (variant.type as Scalar).name === "string") {
309
+
hasStringType = true;
310
+
} else if (variant.type.kind === "Intrinsic") {
311
+
const intrinsicName = (variant.type as any).name;
312
+
if (intrinsicName === "unknown" || intrinsicName === "never") {
313
+
hasUnknown = true;
314
}
315
+
}
316
+
}
317
318
+
if (stringLiterals.length && hasStringType && !unionRefs.length) {
319
+
const primitive: any = { type: "string", knownValues: stringLiterals };
0
320
321
+
const maxLength = getMaxLength(this.program, unionType);
322
+
if (maxLength !== undefined) primitive.maxLength = maxLength;
323
324
+
const minLength = getMinLength(this.program, unionType);
325
+
if (minLength !== undefined) primitive.minLength = minLength;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
326
327
+
const maxGraphemes = getMaxGraphemes(this.program, unionType);
328
+
if (maxGraphemes !== undefined) primitive.maxGraphemes = maxGraphemes;
0
0
0
329
330
+
const minGraphemes = getMinGraphemes(this.program, unionType);
331
+
if (minGraphemes !== undefined) primitive.minGraphemes = minGraphemes;
0
0
0
0
332
333
+
if (prop) {
334
+
const propDesc = getDoc(this.program, prop);
335
+
if (propDesc) primitive.description = propDesc;
336
+
337
+
const defaultValue = (prop as any).default;
338
+
if (defaultValue?.value !== undefined && typeof defaultValue.value === "string") {
339
+
primitive.default = defaultValue.value;
340
+
}
341
}
342
+
return primitive;
343
+
}
344
345
+
if (unionRefs.length) {
346
+
if (stringLiterals.length) {
347
+
this.program.reportDiagnostic({
348
+
code: "union-mixed-refs-literals",
349
+
severity: "error",
350
+
message:
351
+
`Union contains both model references and string literals. Atproto unions must be either: ` +
352
+
`(1) model references only (type: "union"), or ` +
353
+
`(2) string literals + string type (type: "string" with knownValues). ` +
354
+
`Separate these into distinct fields or nested unions.`,
355
+
target: unionType,
356
+
});
357
+
return null;
358
+
}
359
360
+
const unionDef: any = { type: "union", refs: unionRefs };
361
+
362
+
if (isClosed(this.program, unionType)) {
363
+
if (hasUnknown) {
364
this.program.reportDiagnostic({
365
+
code: "closed-open-union",
366
severity: "error",
367
message:
368
+
"@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.",
369
+
target: unionType,
370
});
371
+
} else {
372
+
unionDef.closed = true;
373
+
}
374
+
}
375
376
+
const propDesc = prop ? getDoc(this.program, prop) : undefined;
377
+
return this.addDescription(unionDef, propDesc);
378
+
}
0
0
0
0
0
379
380
+
if (!stringLiterals.length && !hasUnknown) {
381
+
this.program.reportDiagnostic({
382
+
code: "union-empty",
383
+
severity: "error",
384
+
message: `Union has no variants. Atproto unions must contain either model references or string literals.`,
385
+
target: unionType,
386
+
});
387
+
}
0
0
0
0
0
388
389
+
return null;
390
+
}
391
+
392
+
private addOperationToDefs(
393
+
lexicon: LexiconDocument,
394
+
operation: any,
395
+
defName: string,
396
+
) {
397
+
const description = getDoc(this.program, operation);
398
+
399
+
if (isQuery(this.program, operation)) {
400
+
const queryDef: any = { type: "query" };
401
+
this.addDescription(queryDef, description);
402
+
this.addParameters(queryDef, operation);
403
+
this.addOutput(queryDef, operation);
404
+
this.addErrors(queryDef, operation);
405
+
lexicon.defs[defName] = queryDef;
406
+
} else if (isProcedure(this.program, operation)) {
407
+
const procedureDef: any = { type: "procedure" };
408
+
this.addDescription(procedureDef, description);
409
+
this.addProcedureParams(procedureDef, operation);
410
+
this.addOutput(procedureDef, operation);
411
+
this.addErrors(procedureDef, operation);
412
+
lexicon.defs[defName] = procedureDef;
413
+
} else if (isSubscription(this.program, operation)) {
414
+
const subscriptionDef: any = { type: "subscription" };
415
+
this.addDescription(subscriptionDef, description);
416
+
this.addParameters(subscriptionDef, operation);
417
+
this.addMessage(subscriptionDef, operation);
418
+
this.addErrors(subscriptionDef, operation);
419
+
lexicon.defs[defName] = subscriptionDef;
420
+
}
421
+
}
422
423
+
private addParameters(def: any, operation: any) {
424
+
if (!operation.parameters?.properties?.size) return;
0
0
0
0
0
0
425
426
+
const params: any = { type: "params", properties: {} };
427
+
const required: string[] = [];
0
0
0
0
0
0
0
0
428
429
+
for (const [paramName, param] of operation.parameters.properties) {
430
+
const paramDef = this.typeToLexiconDefinition(param.type, param);
431
+
if (paramDef) {
432
+
params.properties[paramName] = paramDef;
433
+
if (!param.optional) required.push(paramName);
434
+
}
435
+
}
0
0
436
437
+
if (required.length) params.required = required;
438
+
def.parameters = params;
439
+
}
0
0
0
0
440
441
+
private addProcedureParams(def: any, operation: any) {
442
+
if (!operation.parameters?.properties?.size) return;
443
444
+
const params = Array.from(operation.parameters.properties) as [string, any][];
445
+
const paramCount = params.length;
0
0
0
0
0
0
0
446
447
+
if (paramCount > 2) {
448
+
this.program.reportDiagnostic({
449
+
code: "procedure-too-many-params",
450
+
severity: "error",
451
+
message: "Procedures can have at most 2 parameters (input and/or parameters)",
452
+
target: operation,
453
+
});
454
+
return;
455
+
}
456
457
+
if (paramCount === 1) {
458
+
const [paramName, param] = params[0];
459
+
if (paramName !== "input") {
460
+
this.program.reportDiagnostic({
461
+
code: "procedure-invalid-param-name",
462
+
severity: "error",
463
+
message: `Procedure parameter must be named "input", got "${paramName}"`,
464
+
target: param,
465
+
});
466
}
467
+
this.addInput(def, param);
468
+
} else if (paramCount === 2) {
469
+
const [param1Name, param1] = params[0];
470
+
const [param2Name, param2] = params[1];
471
472
+
if (param1Name !== "input") {
473
+
this.program.reportDiagnostic({
474
+
code: "procedure-invalid-first-param",
475
+
severity: "error",
476
+
message: `First parameter must be named "input", got "${param1Name}"`,
477
+
target: param1,
478
+
});
0
0
0
0
0
0
0
0
479
}
480
481
+
if (param2Name !== "parameters") {
482
+
this.program.reportDiagnostic({
483
+
code: "procedure-invalid-second-param",
484
+
severity: "error",
485
+
message: `Second parameter must be named "parameters", got "${param2Name}"`,
486
+
target: param2,
487
+
});
488
}
489
490
+
if (param2.type.kind !== "Model" || (param2.type as any).name) {
491
+
this.program.reportDiagnostic({
492
+
code: "procedure-parameters-not-object",
493
+
severity: "error",
494
+
message: "The 'parameters' parameter must be a plain object, not a model reference",
495
+
target: param2,
496
+
});
0
0
497
}
498
499
+
this.addInput(def, param1);
0
0
0
0
0
500
501
+
const parametersModel = param2.type as any;
502
+
if (parametersModel.kind === "Model" && parametersModel.properties) {
503
+
const paramsObj: any = { type: "params", properties: {} };
504
const required: string[] = [];
505
506
+
for (const [propName, prop] of parametersModel.properties) {
507
+
const propDef = this.typeToLexiconDefinition(prop.type, prop);
508
+
if (propDef) {
509
+
paramsObj.properties[propName] = propDef;
510
+
if (!prop.optional) required.push(propName);
0
0
511
}
512
}
513
514
+
if (required.length) paramsObj.required = required;
515
+
def.parameters = paramsObj;
0
0
0
516
}
517
+
}
518
+
}
519
520
+
private addInput(def: any, param: any) {
521
+
const inputSchema = this.typeToLexiconDefinition(param.type);
522
+
if (inputSchema) {
523
+
const encoding = getEncoding(this.program, param) || "application/json";
524
+
def.input = { encoding, schema: inputSchema };
525
+
}
526
+
}
527
+
528
+
private addOutput(def: any, operation: any) {
529
+
const encoding = getEncoding(this.program, operation);
530
+
if (operation.returnType?.kind !== "Intrinsic") {
531
+
const schema = this.typeToLexiconDefinition(operation.returnType);
532
+
if (schema) {
533
+
def.output = { encoding: encoding || "application/json", schema };
0
0
0
0
0
0
534
}
535
+
} else if (encoding) {
536
+
def.output = { encoding };
537
+
}
538
+
}
539
540
+
private addMessage(def: any, operation: any) {
541
+
if (operation.returnType?.kind === "Union") {
542
+
const messageSchema = this.typeToLexiconDefinition(operation.returnType);
543
+
if (messageSchema) {
544
+
def.message = { schema: messageSchema };
545
}
546
+
} else if (operation.returnType?.kind !== "Intrinsic") {
547
+
this.program.reportDiagnostic({
548
+
code: "subscription-return-not-union",
549
+
severity: "error",
550
+
message: "Subscription return type must be a union",
551
+
target: operation,
552
+
});
553
}
554
+
}
555
+
556
+
private addErrors(def: any, operation: any) {
557
+
const errors = getErrors(this.program, operation);
558
+
if (errors?.length) def.errors = errors;
559
}
560
561
private visitModel(model: Model) {
···
628
model: Model,
629
includeModelDescription: boolean = true,
630
): LexiconObject {
0
0
0
631
const required: string[] = [];
632
const nullable: string[] = [];
633
const properties: any = {};
634
635
for (const [name, prop] of model.properties) {
636
+
if (!prop.optional) {
0
637
if (!isRequired(this.program, prop)) {
638
this.program.reportDiagnostic({
639
code: "closed-open-union-inline",
···
648
required.push(name);
649
}
650
0
651
let typeToProcess = prop.type;
652
if (prop.type.kind === "Union") {
653
+
const variants = Array.from((prop.type as Union).variants.values());
0
0
0
654
const hasNull = variants.some(
655
(v) => v.type.kind === "Intrinsic" && (v.type as any).name === "null",
656
);
657
658
if (hasNull) {
0
659
nullable.push(name);
0
0
660
const nonNullVariant = variants.find(
661
+
(v) => !(v.type.kind === "Intrinsic" && (v.type as any).name === "null"),
0
662
);
663
+
if (nonNullVariant) typeToProcess = nonNullVariant.type;
0
0
664
}
665
}
666
667
const propDef = this.typeToLexiconDefinition(typeToProcess, prop);
668
+
if (propDef) properties[name] = propDef;
0
0
0
0
0
0
0
0
0
0
0
669
}
670
671
+
const obj: any = { type: "object" };
672
+
const description = includeModelDescription ? getDoc(this.program, model) : undefined;
673
+
if (description) obj.description = description;
674
+
if (required.length) obj.required = required;
675
+
if (nullable.length) obj.nullable = nullable;
0
0
0
676
obj.properties = properties;
0
677
return obj;
678
}
679
···
682
prop?: ModelProperty,
683
isDefining?: boolean,
684
): LexiconDefinition | null {
685
+
const propDesc = prop ? getDoc(this.program, prop) : undefined;
686
+
687
switch (type.kind) {
688
case "Namespace": {
689
+
const mainModel = (type as any).models?.get("Main");
0
0
690
if (mainModel) {
691
const ref = this.getModelReference(mainModel);
692
+
if (ref) return this.addDescription({ type: "ref", ref }, propDesc);
0
0
0
0
0
0
0
0
0
0
0
0
693
}
694
return null;
695
}
696
case "Enum": {
697
+
const members = Array.from((type as any).members?.values?.() || []);
0
0
698
const values = members.map((m: any) => m.value);
0
0
699
const firstValue = values[0];
0
0
0
700
701
+
if (typeof firstValue === "string") {
702
+
return this.addDescription({ type: "string", enum: values }, propDesc);
703
+
} else if (typeof firstValue === "number" && Number.isInteger(firstValue)) {
704
+
return this.addDescription({ type: "integer", enum: values }, propDesc);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
705
}
0
706
return null;
707
}
708
case "Boolean": {
709
+
return this.addDescription({
0
0
710
type: "boolean",
711
+
const: (type as any).value
712
+
}, propDesc);
0
0
0
0
0
0
0
713
}
714
case "Scalar":
715
const scalar = type as Scalar;
716
const primitive = this.scalarToLexiconPrimitive(scalar, prop);
717
+
if (!primitive) return null;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
718
719
+
if (propDesc) {
720
+
primitive.description = propDesc;
721
+
} else if (scalar.baseScalar && scalar.namespace?.name !== "TypeSpec") {
722
+
const FORMAT_SCALARS = new Set([
723
+
"datetime", "did", "handle", "atUri", "cid", "tid", "nsid",
724
+
"recordKey", "uri", "language", "atIdentifier", "bytes",
725
+
"utcDateTime", "offsetDateTime", "plainDate", "plainTime",
726
+
]);
727
+
if (!FORMAT_SCALARS.has(scalar.name)) {
728
+
const scalarDesc = getDoc(this.program, scalar);
729
+
if (scalarDesc) primitive.description = scalarDesc;
730
}
731
}
732
return primitive;
733
case "Model":
0
734
const model = type as Model;
735
0
0
736
const isBlobModel =
737
isBlob(this.program, model) ||
738
(isTemplateInstance(model) && model.templateNode && isBlob(this.program, model.templateNode as any)) ||
739
(model.baseModel && isBlob(this.program, model.baseModel));
740
741
if (isBlobModel) {
742
+
return this.addDescription(this.createBlobDef(model), propDesc);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
743
}
744
0
745
const isClosedModel =
746
model.name === "Closed" ||
747
(model.node && (model.node as any).symbol?.name === "Closed") ||
748
+
(isTemplateInstance(model) && model.node && (model.node as any).symbol?.name === "Closed");
0
0
749
750
if (isClosedModel && isTemplateInstance(model)) {
751
+
const unionArg = model.templateMapper?.args?.[0];
752
+
if (unionArg && isType(unionArg) && unionArg.kind === "Union") {
753
+
const unionDef = this.typeToLexiconDefinition(unionArg, prop);
754
+
if (unionDef && unionDef.type === "union") {
755
+
(unionDef as LexiconUnion).closed = true;
756
+
return unionDef;
0
0
0
0
0
757
}
758
}
759
}
760
761
+
const modelRef = this.getModelReference(model);
0
0
762
if (modelRef) {
763
+
return this.addDescription({ type: "ref", ref: modelRef }, propDesc);
0
0
0
0
0
0
0
0
0
0
764
}
765
766
+
if (isArrayModelType(this.program, model)) {
767
+
return this.addDescription(this.modelToLexiconArray(model, prop), propDesc);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
768
}
769
+
770
+
return this.addDescription(this.modelToLexiconObject(model), propDesc);
771
case "Union":
0
772
const unionType = type as Union;
773
0
0
774
if (!isDefining) {
775
const unionRef = this.getUnionReference(unionType);
776
if (unionRef) {
777
+
return this.addDescription({ type: "ref", ref: unionRef }, propDesc);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
778
}
779
}
780
781
+
return this.processUnion(unionType, prop);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
782
case "Intrinsic":
783
+
return this.addDescription({ type: "unknown" }, propDesc);
0
0
0
0
0
0
0
0
0
0
784
default:
785
return null;
786
}
···
790
scalar: Scalar,
791
prop?: ModelProperty,
792
): LexiconDefinition | null {
793
+
const FORMAT_MAP: Record<string, string> = {
794
+
did: "did", handle: "handle", atUri: "at-uri", datetime: "datetime",
795
+
cid: "cid", tid: "tid", nsid: "nsid", recordKey: "record-key",
796
+
uri: "uri", language: "language", atIdentifier: "at-identifier",
797
+
utcDateTime: "datetime", offsetDateTime: "datetime",
798
+
plainDate: "datetime", plainTime: "datetime"
799
};
800
801
+
if (scalar.name === "bytes") {
802
+
if (prop) {
803
+
const accept = getBlobAccept(this.program, prop);
804
+
const maxSize = getBlobMaxSize(this.program, prop);
805
+
if (accept || maxSize !== undefined) {
806
+
const blobDef: LexiconBlob = { type: "blob" };
807
+
if (accept) blobDef.accept = accept;
808
+
if (maxSize !== undefined) blobDef.maxSize = maxSize;
809
+
return blobDef;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
810
}
811
+
}
812
+
return { type: "bytes" };
0
0
0
0
813
}
814
815
+
const primitive: any = { type: "string" };
816
+
817
+
if (scalar.name === "boolean") primitive.type = "boolean";
818
+
else if (["integer", "int32", "int64", "int16", "int8"].includes(scalar.name)) primitive.type = "integer";
819
+
else if (["float32", "float64"].includes(scalar.name)) primitive.type = "number";
820
821
+
const format = FORMAT_MAP[scalar.name] || getLexFormat(this.program, prop || scalar);
822
+
if (format) primitive.format = format;
0
0
823
824
+
const target = prop || scalar;
825
const maxLength = getMaxLength(this.program, target);
826
+
if (maxLength !== undefined) primitive.maxLength = maxLength;
0
0
0
827
const minLength = getMinLength(this.program, target);
828
+
if (minLength !== undefined) primitive.minLength = minLength;
0
0
0
829
const maxGraphemes = getMaxGraphemes(this.program, target);
830
+
if (maxGraphemes !== undefined) primitive.maxGraphemes = maxGraphemes;
0
0
0
831
const minGraphemes = getMinGraphemes(this.program, target);
832
+
if (minGraphemes !== undefined) primitive.minGraphemes = minGraphemes;
0
0
833
0
834
if (prop) {
0
835
const constValue = getLexConst(this.program, prop);
836
+
if (constValue !== undefined &&
837
+
(primitive.type === "boolean" && typeof constValue === "boolean" ||
838
+
primitive.type === "string" && typeof constValue === "string" ||
839
+
primitive.type === "integer" && typeof constValue === "number")) {
840
+
primitive.const = constValue;
0
0
0
0
0
0
0
0
0
841
}
842
843
+
const defaultValue = (prop as any).default?.value;
844
+
if (defaultValue !== undefined &&
845
+
(primitive.type === "string" && typeof defaultValue === "string" ||
846
+
primitive.type === "integer" && typeof defaultValue === "number" ||
847
+
primitive.type === "boolean" && typeof defaultValue === "boolean")) {
848
+
primitive.default = defaultValue;
0
0
0
0
0
849
}
0
850
851
+
if (primitive.type === "integer" || primitive.type === "number") {
852
+
const minValue = getMinValue(this.program, prop);
853
+
if (minValue !== undefined) primitive.minimum = minValue;
854
+
const maxValue = getMaxValue(this.program, prop);
855
+
if (maxValue !== undefined) primitive.maximum = maxValue;
0
0
0
0
856
}
857
}
858
···
860
}
861
862
private getModelReference(model: Model): string | null {
863
+
if (!model.name || !model.namespace || model.namespace.name === "TypeSpec") return null;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
864
865
const namespaceName = getNamespaceFullName(model.namespace);
866
+
if (!namespaceName) return null;
0
0
867
868
const defName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
869
870
+
if (this.currentLexiconId === namespaceName || this.currentLexiconId === `${namespaceName}.defs`) {
871
+
return `#${defName}`;
0
0
0
0
0
0
0
0
872
}
873
874
+
return model.name === "Main" ? namespaceName : `${namespaceName}#${defName}`;
0
0
0
0
0
0
0
875
}
876
877
private getUnionReference(union: Union): string | null {
0
878
const unionName = (union as any).name;
0
0
0
0
0
879
const namespace = (union as any).namespace;
880
+
if (!unionName || !namespace || namespace.name === "TypeSpec") return null;
0
0
881
882
const namespaceName = getNamespaceFullName(namespace);
883
+
if (!namespaceName) return null;
0
0
884
885
const defName = unionName.charAt(0).toLowerCase() + unionName.slice(1);
886
887
+
if (this.currentLexiconId === namespaceName || this.currentLexiconId === `${namespaceName}.defs`) {
888
+
return `#${defName}`;
0
0
0
0
0
0
0
0
889
}
890
0
891
return `${namespaceName}#${defName}`;
892
}
893
···
895
model: Model,
896
prop?: ModelProperty,
897
): LexiconArray | null {
0
0
898
const arrayModel = model.sourceModel || model;
899
+
const itemType = arrayModel.templateMapper?.args?.[0];
900
901
+
if (itemType && isType(itemType)) {
902
+
const itemDef = this.typeToLexiconDefinition(itemType);
903
+
if (!itemDef) return null;
904
905
+
const arrayDef: LexiconArray = { type: "array", items: itemDef };
0
906
907
+
if (prop) {
908
+
const maxItems = getMaxItems(this.program, prop);
909
+
if (maxItems !== undefined) arrayDef.maxLength = maxItems;
910
+
const minItems = getMinItems(this.program, prop);
911
+
if (minItems !== undefined) arrayDef.minLength = minItems;
912
+
}
0
0
0
0
0
0
913
914
+
return arrayDef;
0
0
0
0
0
0
0
0
915
}
916
917
return null;
918
}
919
920
private getModelLexiconId(model: Model): string | null {
921
+
if (!model.namespace) return null;
0
0
922
923
const namespaceName = getNamespaceFullName(model.namespace);
924
+
if (!namespaceName) return null;
0
0
925
926
+
if (model.name === "Main") return namespaceName;
0
0
0
0
927
0
928
return `${namespaceName}.${model.name.charAt(0).toLowerCase() + model.name.slice(1)}`;
929
}
930
931
private getLexiconPath(lexiconId: string): string {
0
932
const parts = lexiconId.split(".");
933
+
return join(this.options.outputDir, ...parts.slice(0, -1), parts[parts.length - 1] + ".json");
0
0
0
934
}
935
936
private async writeFile(filePath: string, content: string) {