A tool for conquest of ATProto lexicons. https://jsr.io/@hotsocket/lexiconqueror
8.0 kB
269 lines
1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7import ts, { factory as f, SyntaxKind } from "typescript";
8import type { lexicon as lex } from "@hotsocket/atproto-common";
9
10/* need to work out the flow
11
12if theres a main def, wrap the rest in a namespace, since the default export needs to be the main def
13^> If a main definition exists, it can be referenced without a fragment, just using the NSID.
14 > For references in the $type fields in data objects themselves (eg, records or contents of a union),
15 > this is a "must" (use of a #main suffix is invalid). For example, com.example.record not com.example.record#main.
16
17then we rename defs not starting with _ to start with $, to create that visual differentiation like lexicon refs
18^ com.atproto.label.defs#selfLabels -> AT.com.atproto.label.defs.$selfLabels
19
20also with defs containing an "unknown" type, add a type parameter to the output type, and set the field type T
21
22 */
23
24export const PRINTER = ts.createPrinter({
25 newLine: ts.NewLineKind.LineFeed,
26 noEmitHelpers: false,
27 omitTrailingSemicolon: false,
28 removeComments: false,
29});
30
31export const CONSTRAINT_SERIALIZABLEOBJECT = f.createTypeParameterDeclaration(
32 undefined,
33 "T",
34 f.createTypeReferenceNode("common.types.SerializableObject"),
35 f.createTypeReferenceNode("common.types.SerializableObject"),
36);
37export const CONSTRAINT_SERIALIZABLEPARAMS = f.createTypeParameterDeclaration(
38 undefined,
39 "T",
40 f.createTypeReferenceNode("common.types.SerializableParams"),
41);
42
43export type ImportInfo = {
44 defaultImport?: string;
45 namedImports?: ts.ImportSpecifier[];
46 nsImport?: string;
47};
48export function finalizeImports(
49 imports: Record<string, ImportInfo>,
50): ts.Statement[] {
51 const output: ts.Statement[] = [];
52 for (
53 const [module, { defaultImport, namedImports, nsImport }] of Object.entries(imports)
54 ) {
55 if (nsImport) {
56 output.push(
57 f.createImportDeclaration(
58 undefined,
59 f.createImportClause(
60 undefined,
61 undefined,
62 f.createNamespaceImport(f.createIdentifier(nsImport)),
63 ),
64 f.createStringLiteral(module),
65 ),
66 );
67 } else {
68 output.push(f.createImportDeclaration(
69 undefined,
70 f.createImportClause(
71 undefined,
72 defaultImport ? f.createIdentifier(defaultImport) : undefined,
73 namedImports ? f.createNamedImports(namedImports) : undefined,
74 ),
75 f.createStringLiteral(module),
76 ));
77 }
78 }
79 return output;
80}
81
82export function addComments<T extends lex.SchemaObject>(
83 def: T,
84 declaration: ts.Statement | ts.Declaration,
85) {
86 const commentParts: string[] = [];
87 if (def.description) commentParts.push(def.description);
88 // if (
89 // def.type == "query" || def.type == "procedure" || def.type == "subscription"
90 // ) commentParts.push("@" + def.type);
91 commentParts.push(`@lextype ${def.type}`);
92 if (def.type === "string" && "format" in def && (def as lex.String).format) {
93 commentParts.push("@format " + (def as lex.String).format);
94 }
95 if (commentParts.length > 0) {
96 ts.addSyntheticLeadingComment(
97 declaration,
98 ts.SyntaxKind.MultiLineCommentTrivia,
99 "* " + commentParts.join("\n * ") + " ",
100 true,
101 );
102 }
103}
104export function addNamedImport(
105 imports: Record<string, ImportInfo>,
106 name: string,
107 file: string,
108) {
109 if (!imports[file]) imports[file] = {};
110 if (!imports[file].namedImports) imports[file].namedImports = [];
111 if (imports[file].namedImports.find((x) => x.name.text == name)) return;
112 imports[file].namedImports.push(
113 f.createImportSpecifier(false, undefined, f.createIdentifier(name)),
114 );
115}
116
117export function lastNSIDPart(nsid: string): string {
118 return nsid.substring(nsid.lastIndexOf(".") + 1);
119}
120
121export function refToNode(
122 ref: string,
123 lex: lex.Lexicon,
124 imports: Record<string, ImportInfo>,
125) {
126 const fixedRef = ref.replace("#", ".$");
127 // #something means it's local to "here"
128 if (ref.startsWith("#")) {
129 if (Object.hasOwn(lex.defs, "main")) {
130 //${lastNSIDPart(lex.id)}${fixedRef}
131 return f.createTypeReferenceNode(`${lastNSIDPart(lex.id)}${fixedRef}`);
132 } else {
133 return f.createTypeReferenceNode(fixedRef.replace(".", ""));
134 }
135 } else {
136 imports["@/index.ts"] = { nsImport: "AT" };
137 return f.createTypeReferenceNode("AT." + fixedRef);
138 }
139}
140
141export function lookupType(
142 object: lex.AnySchemaObject,
143 lex: lex.Lexicon,
144 name: string,
145 imports: Record<string, ImportInfo>,
146): ts.TypeNode {
147 switch (object.type) {
148 case "cid-link":
149 case "bytes":
150 case "token":
151 case "string":
152 return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
153 case "blob":
154 imports["@hotsocket/atproto-common"] = { nsImport: "common" };
155 return f.createTypeReferenceNode("common.types.XBlob");
156 case "boolean":
157 return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
158 case "integer":
159 return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
160 case "unknown":
161 return f.createTypeReferenceNode("T");
162 case "ref":
163 return refToNode(object.ref, lex, imports);
164 case "union":
165 // TODO: Handle union types properly
166 if (object.refs.length == 0) {
167 // Record<PropertyKey, never>
168 return f.createTypeReferenceNode(
169 "Record",
170 [
171 f.createTypeReferenceNode("PropertyKey"),
172 f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
173 ],
174 );
175 } else {
176 return f.createUnionTypeNode(
177 object.refs.map((ref) => refToNode(ref, lex, imports)),
178 );
179 }
180 case "array":
181 // TODO: Handle array types properly
182 return f.createArrayTypeNode(
183 lookupType(object.items, lex, name + ">array", imports),
184 );
185 // objects, params are handled differently
186 default:
187 throw new Error(`unhandled type ${object.type} for ${lex.id}#${name}`);
188 }
189}
190
191export function propsContainUnknown(
192 props: Record<string, lex.AnySchemaObject>,
193): boolean {
194 for (const name in props) {
195 const prop = props[name]!;
196 if (prop.type == "unknown") return true;
197 if (prop.type == "array" && prop.items.type == "unknown") return true;
198 }
199 return false;
200}
201
202export function convertProperty(
203 lex: lex.Lexicon,
204 name: string,
205 def: lex.FieldTypes,
206 imports: Record<string, ImportInfo>,
207): ts.Statement {
208 return f.createTypeAliasDeclaration(
209 [
210 f.createModifier(ts.SyntaxKind.ExportKeyword),
211 ],
212 name,
213 undefined,
214 lookupType(def, lex, name, imports),
215 );
216}
217
218export function convertSchemaObject(
219 lex: lex.Lexicon,
220 name: string,
221 def: lex.Object,
222 imports: Record<string, ImportInfo>,
223): ts.Statement | void {
224 const members: ts.TypeElement[] = [];
225 imports["@hotsocket/atproto-common"] = { nsImport: "common" };
226 // members.push(
227 // `\t[key: string]: ${name == "_parameters" ? "string | number | boolean | null | undefined" : "Serializable"};`,
228 // );
229 function requiredLookup(propName: string): ts.QuestionToken | undefined {
230 if (def.required && def.required.indexOf(propName) != -1) {
231 return undefined;
232 } else {
233 return f.createToken(ts.SyntaxKind.QuestionToken);
234 }
235 }
236 // detect whether any property contains `unknown` (including arrays of unknown)
237 // so we can add the generic `T` type parameter to the interface when needed.
238 const hasUnknown = propsContainUnknown(def.properties);
239 for (const propName in def.properties) {
240 const prop = def.properties[propName]!;
241
242 const member = f.createPropertySignature(
243 undefined,
244 propName,
245 requiredLookup(propName),
246 lookupType(prop, lex, propName, imports),
247 );
248 addComments(prop, member);
249 members.push(member);
250 }
251 let inheritID;
252 if (name == "_parameters") {
253 inheritID = f.createIdentifier("common.types.SerializableParams");
254 } else {
255 inheritID = f.createIdentifier("common.types.SerializableObject");
256 }
257 return f.createInterfaceDeclaration(
258 [f.createModifier(ts.SyntaxKind.ExportKeyword)],
259 name,
260 hasUnknown ? [CONSTRAINT_SERIALIZABLEOBJECT] : undefined,
261 [f.createHeritageClause(SyntaxKind.ExtendsKeyword, [
262 f.createExpressionWithTypeArguments(
263 inheritID,
264 undefined,
265 ),
266 ])],
267 members,
268 );
269}