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