@nexus-rpc/gen-core 0.1.0-alpha0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/definition-schema.d.ts +29 -0
- package/dist/definition-schema.js +3 -0
- package/dist/generator.d.ts +53 -0
- package/dist/generator.js +128 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/language-csharp.d.ts +4 -0
- package/dist/language-csharp.js +107 -0
- package/dist/language-go.d.ts +15 -0
- package/dist/language-go.js +213 -0
- package/dist/language-java.d.ts +5 -0
- package/dist/language-java.js +111 -0
- package/dist/language-python.d.ts +4 -0
- package/dist/language-python.js +181 -0
- package/dist/language-typescript.d.ts +5 -0
- package/dist/language-typescript.js +179 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +34 -0
- package/dist/render-adapter.d.ts +44 -0
- package/dist/render-adapter.js +88 -0
- package/dist/utility.d.ts +10 -0
- package/dist/utility.js +30 -0
- package/package.json +31 -0
- package/src/definition-schema.ts +22 -0
- package/src/generator.ts +222 -0
- package/src/index.ts +8 -0
- package/src/language-csharp.ts +190 -0
- package/src/language-go.ts +310 -0
- package/src/language-java.ts +191 -0
- package/src/language-python.ts +260 -0
- package/src/language-typescript.ts +288 -0
- package/src/parser.ts +47 -0
- package/src/render-adapter.ts +205 -0
- package/src/utility.ts +52 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClassType,
|
|
3
|
+
ConvenienceRenderer,
|
|
4
|
+
Name,
|
|
5
|
+
Namer,
|
|
6
|
+
pythonOptions,
|
|
7
|
+
PythonRenderer,
|
|
8
|
+
PythonTargetLanguage,
|
|
9
|
+
Type,
|
|
10
|
+
type LanguageName,
|
|
11
|
+
type OptionValues,
|
|
12
|
+
type RenderContext,
|
|
13
|
+
type RendererOptions,
|
|
14
|
+
type Sourcelike,
|
|
15
|
+
} from "quicktype-core";
|
|
16
|
+
import { RenderAdapter, type RenderAccessible } from "./render-adapter.js";
|
|
17
|
+
import type { PreparedService, PreparedTypeReference } from "./generator.js";
|
|
18
|
+
import { splitDescription } from "./utility.js";
|
|
19
|
+
|
|
20
|
+
// Change some defaults globally
|
|
21
|
+
pythonOptions.justTypes.definition.defaultValue = true;
|
|
22
|
+
pythonOptions.features.definition.defaultValue = "3.7";
|
|
23
|
+
pythonOptions.nicePropertyNames.definition.defaultValue = true;
|
|
24
|
+
pythonOptions.pydanticBaseModel.definition.defaultValue = true;
|
|
25
|
+
|
|
26
|
+
export class PythonLanguageWithNexus extends PythonTargetLanguage {
|
|
27
|
+
protected override makeRenderer<Lang extends LanguageName = "python">(
|
|
28
|
+
renderContext: RenderContext,
|
|
29
|
+
untypedOptionValues: RendererOptions<Lang>,
|
|
30
|
+
): PythonRenderer {
|
|
31
|
+
const adapter = new PythonRenderAdapter(
|
|
32
|
+
super.makeRenderer(renderContext, untypedOptionValues),
|
|
33
|
+
untypedOptionValues,
|
|
34
|
+
);
|
|
35
|
+
adapter.assertValidOptions();
|
|
36
|
+
return adapter.makeRenderer({
|
|
37
|
+
emitSourceStructure(original, givenOutputFilename) {
|
|
38
|
+
// Generated comment
|
|
39
|
+
adapter.render.emitLine("# Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
40
|
+
adapter.render.ensureBlankLine();
|
|
41
|
+
|
|
42
|
+
// We need to add the future annotation
|
|
43
|
+
// TODO(cretz): Have option to remove this?
|
|
44
|
+
adapter.render.emitLine("from __future__ import annotations");
|
|
45
|
+
adapter.render.ensureBlankLine();
|
|
46
|
+
original(givenOutputFilename);
|
|
47
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
48
|
+
},
|
|
49
|
+
emitClosingCode(original) {
|
|
50
|
+
original();
|
|
51
|
+
// Emit services _after_ all types are present. We choose to be after
|
|
52
|
+
// the model types so we don't have any "ForwardRef"s to any models
|
|
53
|
+
// which is not supported by Nexus Python's handler type-checking.
|
|
54
|
+
adapter.emitServices();
|
|
55
|
+
},
|
|
56
|
+
emitClass(_original, t) {
|
|
57
|
+
adapter.emitClass(t);
|
|
58
|
+
},
|
|
59
|
+
emitImports(original) {
|
|
60
|
+
original();
|
|
61
|
+
adapter.emitAdditionalImports();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type PythonRenderAccessible = PythonRenderer &
|
|
68
|
+
RenderAccessible & {
|
|
69
|
+
readonly pyOptions: OptionValues<typeof pythonOptions>;
|
|
70
|
+
declareType<T extends Type>(t: T, emitter: () => void): void;
|
|
71
|
+
emitBlock(line: Sourcelike, f: () => void): void;
|
|
72
|
+
emitClass(t: ClassType): void;
|
|
73
|
+
emitClosingCode(): void;
|
|
74
|
+
emitImports(): void;
|
|
75
|
+
emitSourceStructure(givenOutputFilename: string): void;
|
|
76
|
+
pythonType(t: Type, isRootTypeDef: boolean): Sourcelike;
|
|
77
|
+
string(s: string): Sourcelike;
|
|
78
|
+
typeHint(...sl: Sourcelike[]): Sourcelike;
|
|
79
|
+
withImport(module: string, name: string): Sourcelike;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const emptyNameMap: ReadonlyMap<Name, string> = new Map();
|
|
83
|
+
|
|
84
|
+
class PythonRenderAdapter extends RenderAdapter<PythonRenderAccessible> {
|
|
85
|
+
private readonly typeNamer: Namer;
|
|
86
|
+
private readonly propertyNamer: Namer;
|
|
87
|
+
|
|
88
|
+
constructor(render: ConvenienceRenderer, rendererOptions: RendererOptions) {
|
|
89
|
+
super(render, rendererOptions);
|
|
90
|
+
this.typeNamer = this.render.makeNamedTypeNamer();
|
|
91
|
+
this.propertyNamer = this.render.namerForObjectProperty();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assertValidOptions() {
|
|
95
|
+
// Currently, we only support specific Python options
|
|
96
|
+
if (!this.render.pyOptions.justTypes) {
|
|
97
|
+
throw new Error("Python option for just-types must be set");
|
|
98
|
+
}
|
|
99
|
+
if (!this.render.pyOptions.pydanticBaseModel) {
|
|
100
|
+
throw new Error("Python option for pydantic-model must be set");
|
|
101
|
+
}
|
|
102
|
+
if (!this.render.pyOptions.features.typeHints) {
|
|
103
|
+
throw new Error("Python option to include type hints must be set");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
makeFileName() {
|
|
108
|
+
// If there is a single service, use that, otherwise use the
|
|
109
|
+
// filename sans extensions to build it
|
|
110
|
+
const services = Object.entries(this.schema.services);
|
|
111
|
+
const name =
|
|
112
|
+
services.length == 1
|
|
113
|
+
? services[0][0]
|
|
114
|
+
: this.nexusRendererOptions.firstFilenameSansExtensions;
|
|
115
|
+
return this.propertyNamer.nameStyle(name) + ".py";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
emitAdditionalImports() {
|
|
119
|
+
// We have to emit imports for existing types with dots. Unlike the existing
|
|
120
|
+
// withImport/emitImports, we do not want from X import Y, we want just
|
|
121
|
+
// import and we will fully-qualify the types at the usage site.
|
|
122
|
+
const toImport: string[] = [];
|
|
123
|
+
for (const svcSchema of Object.values(this.schema.services)) {
|
|
124
|
+
for (const opSchema of Object.values(svcSchema.operations)) {
|
|
125
|
+
for (const type of [opSchema.input, opSchema.output]) {
|
|
126
|
+
if (type?.kind == "existing") {
|
|
127
|
+
const lastDot = type.name.lastIndexOf(".");
|
|
128
|
+
if (
|
|
129
|
+
lastDot >= 0 &&
|
|
130
|
+
!toImport.includes(type.name.slice(0, lastDot))
|
|
131
|
+
) {
|
|
132
|
+
toImport.push(type.name.slice(0, lastDot));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
toImport.sort();
|
|
139
|
+
for (const module_ of toImport) {
|
|
140
|
+
this.render.emitLine("import ", module_);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
emitServices() {
|
|
145
|
+
for (const [serviceName, serviceSchema] of Object.entries(
|
|
146
|
+
this.schema.services,
|
|
147
|
+
)) {
|
|
148
|
+
this.emitService(serviceName, serviceSchema);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emitService(serviceName: string, serviceSchema: PreparedService) {
|
|
153
|
+
this.render.ensureBlankLine(2);
|
|
154
|
+
const typeName = this.makeServiceTypeName(
|
|
155
|
+
this.typeNamer.nameStyle(serviceName),
|
|
156
|
+
);
|
|
157
|
+
// Decorator
|
|
158
|
+
this.render.emitLine(
|
|
159
|
+
"@",
|
|
160
|
+
this.render.withImport("nexusrpc", "service"),
|
|
161
|
+
serviceName == typeName
|
|
162
|
+
? []
|
|
163
|
+
: ["(name=", this.render.string(serviceName), ")"],
|
|
164
|
+
);
|
|
165
|
+
// Service class with each property
|
|
166
|
+
this.render.emitBlock(["class ", typeName, ":"], () => {
|
|
167
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
168
|
+
if (Object.entries(serviceSchema.operations).length == 0) {
|
|
169
|
+
this.render.emitLine("pass");
|
|
170
|
+
}
|
|
171
|
+
const propertyNamesInUse = {};
|
|
172
|
+
this.render.forEachWithBlankLines(
|
|
173
|
+
Object.entries(serviceSchema.operations),
|
|
174
|
+
"interposing",
|
|
175
|
+
(op, opName, pos) => {
|
|
176
|
+
const propertyName = this.makeOperationFunctionName(
|
|
177
|
+
this.propertyNamer.nameStyle(opName),
|
|
178
|
+
propertyNamesInUse,
|
|
179
|
+
);
|
|
180
|
+
this.render.emitLine(
|
|
181
|
+
propertyName,
|
|
182
|
+
": ",
|
|
183
|
+
this.render.withImport("nexusrpc", "Operation"),
|
|
184
|
+
"[",
|
|
185
|
+
this.getNexusType(op.input) ?? "None",
|
|
186
|
+
", ",
|
|
187
|
+
this.getNexusType(op.output) ?? "None",
|
|
188
|
+
"]",
|
|
189
|
+
opName == propertyName
|
|
190
|
+
? []
|
|
191
|
+
: [
|
|
192
|
+
" = ",
|
|
193
|
+
this.render.withImport("nexusrpc", "Operation"),
|
|
194
|
+
"(name=",
|
|
195
|
+
this.render.string(opName),
|
|
196
|
+
")",
|
|
197
|
+
],
|
|
198
|
+
);
|
|
199
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
emitClass(t: ClassType) {
|
|
206
|
+
this.render.declareType(t, () => {
|
|
207
|
+
if (t.getProperties().size === 0) {
|
|
208
|
+
this.render.emitLine("pass");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.render.forEachClassProperty(t, "none", (name, jsonName, cp) => {
|
|
212
|
+
// Get the type string and append a pydantic Field to it if the name
|
|
213
|
+
// doesn't match the JSON name.
|
|
214
|
+
let typeSource = this.render.pythonType(cp.type, true);
|
|
215
|
+
// const fieldName = name.namingFunction.nameStyle(name.firstProposedName(emptyNameMap));
|
|
216
|
+
const fieldName = this.render.sourcelikeToString(name);
|
|
217
|
+
if (fieldName != jsonName) {
|
|
218
|
+
// Ellipsis means no default. In optional situations, typeSource has a
|
|
219
|
+
// trailing " = None" which we need to remove and set as default
|
|
220
|
+
let fieldDefault = "...";
|
|
221
|
+
if (Array.isArray(typeSource) && typeSource.at(-1) == " = None") {
|
|
222
|
+
typeSource = typeSource.slice(0, -1);
|
|
223
|
+
fieldDefault = "None";
|
|
224
|
+
}
|
|
225
|
+
typeSource = [
|
|
226
|
+
typeSource,
|
|
227
|
+
" = ",
|
|
228
|
+
this.render.withImport("pydantic", "Field"),
|
|
229
|
+
"(",
|
|
230
|
+
fieldDefault,
|
|
231
|
+
", serialization_alias=",
|
|
232
|
+
this.render.string(jsonName),
|
|
233
|
+
")",
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
this.render.emitLine(name, ": ", typeSource);
|
|
237
|
+
this.render.emitDescription(
|
|
238
|
+
this.render.descriptionForClassProperty(t, jsonName),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
this.render.ensureBlankLine();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getNexusType(
|
|
246
|
+
reference: PreparedTypeReference | undefined,
|
|
247
|
+
): Sourcelike | undefined {
|
|
248
|
+
if (!reference) {
|
|
249
|
+
return undefined;
|
|
250
|
+
} else if (reference.kind == "existing") {
|
|
251
|
+
return reference.name;
|
|
252
|
+
} else {
|
|
253
|
+
const type = this.render.topLevels.get(reference.name);
|
|
254
|
+
if (!type) {
|
|
255
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
256
|
+
}
|
|
257
|
+
return this.render.pythonType(type, false);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClassType,
|
|
3
|
+
Name,
|
|
4
|
+
tsFlowOptions,
|
|
5
|
+
Type,
|
|
6
|
+
TypeScriptRenderer,
|
|
7
|
+
TypeScriptTargetLanguage,
|
|
8
|
+
type LanguageName,
|
|
9
|
+
type MultiWord,
|
|
10
|
+
type RenderContext,
|
|
11
|
+
type RendererOptions,
|
|
12
|
+
type Sourcelike,
|
|
13
|
+
} from "quicktype-core";
|
|
14
|
+
import { RenderAdapter, type RenderAccessible } from "./render-adapter.js";
|
|
15
|
+
import type { PreparedService, PreparedTypeReference } from "./generator.js";
|
|
16
|
+
import { splitDescription } from "./utility.js";
|
|
17
|
+
import { utf16StringEscape } from "quicktype-core/dist/support/Strings.js";
|
|
18
|
+
|
|
19
|
+
// Change some defaults globally
|
|
20
|
+
tsFlowOptions.justTypes.definition.defaultValue = true;
|
|
21
|
+
|
|
22
|
+
export class TypeScriptLanguageWithNexus extends TypeScriptTargetLanguage {
|
|
23
|
+
protected override get defaultIndentation(): string {
|
|
24
|
+
// We want two-space indent to be default for TypeScript
|
|
25
|
+
return " ";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected override makeRenderer<Lang extends LanguageName = "typescript">(
|
|
29
|
+
renderContext: RenderContext,
|
|
30
|
+
untypedOptionValues: RendererOptions<Lang>,
|
|
31
|
+
): TypeScriptRenderer {
|
|
32
|
+
const adapter = new TypeScriptRenderAdapter(
|
|
33
|
+
super.makeRenderer(renderContext, untypedOptionValues),
|
|
34
|
+
untypedOptionValues,
|
|
35
|
+
);
|
|
36
|
+
return adapter.makeRenderer({
|
|
37
|
+
emitSourceStructure(original) {
|
|
38
|
+
adapter.emitServices();
|
|
39
|
+
original();
|
|
40
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
41
|
+
},
|
|
42
|
+
emitTypes(original) {
|
|
43
|
+
// We cannot use original emitTypes, see override for reason why
|
|
44
|
+
adapter.emitTypes();
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type TypeScriptRenderAccessible = TypeScriptRenderer &
|
|
51
|
+
RenderAccessible & {
|
|
52
|
+
emitBlock(source: Sourcelike, end: Sourcelike, emit: () => void): void;
|
|
53
|
+
emitSourceStructure(): void;
|
|
54
|
+
emitTypes(): void;
|
|
55
|
+
nameStyle(original: string, upper: boolean): string;
|
|
56
|
+
sourceFor(t: Type): MultiWord;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface ExistingType {
|
|
60
|
+
type: string;
|
|
61
|
+
from?: string;
|
|
62
|
+
alias?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class TypeScriptRenderAdapter extends RenderAdapter<TypeScriptRenderAccessible> {
|
|
66
|
+
private _existingTypes?: Record<string, ExistingType>;
|
|
67
|
+
|
|
68
|
+
makeFileName() {
|
|
69
|
+
// If there is a single service, use that, otherwise use the
|
|
70
|
+
// filename sans extensions to build it
|
|
71
|
+
const services = Object.entries(this.schema.services);
|
|
72
|
+
if (services.length == 1) {
|
|
73
|
+
return `${services[0][0]}.ts`;
|
|
74
|
+
}
|
|
75
|
+
return `${this.nexusRendererOptions.firstFilenameSansExtensions}.ts`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Key is full string as given in schema
|
|
79
|
+
get existingTypes(): Record<string, ExistingType> {
|
|
80
|
+
if (this._existingTypes === undefined) {
|
|
81
|
+
this._existingTypes = {};
|
|
82
|
+
const inUse = {};
|
|
83
|
+
for (const serviceSchema of Object.values(this.schema.services)) {
|
|
84
|
+
for (const opSchema of Object.values(serviceSchema.operations)) {
|
|
85
|
+
for (const type of [opSchema.input, opSchema.output]) {
|
|
86
|
+
if (type?.kind == "existing") {
|
|
87
|
+
// TODO(cretz): Generics with qualified type args that need to be imported?
|
|
88
|
+
const lastHash = type.name.lastIndexOf("#");
|
|
89
|
+
const existingType: ExistingType = {
|
|
90
|
+
type: type.name.slice(lastHash + 1),
|
|
91
|
+
from: lastHash == -1 ? undefined : type.name.slice(0, lastHash),
|
|
92
|
+
};
|
|
93
|
+
// Alias it while it already exists
|
|
94
|
+
let alias = existingType.type;
|
|
95
|
+
for (
|
|
96
|
+
let index = 1;
|
|
97
|
+
Object.hasOwn(inUse, alias) ||
|
|
98
|
+
this.topLevelNameInUse(alias, true);
|
|
99
|
+
index++
|
|
100
|
+
) {
|
|
101
|
+
alias = `${existingType.type}${index}`;
|
|
102
|
+
}
|
|
103
|
+
if (alias != existingType.type) {
|
|
104
|
+
existingType.alias = alias;
|
|
105
|
+
}
|
|
106
|
+
this._existingTypes[type.name] = existingType;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return this._existingTypes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
emitServices() {
|
|
116
|
+
// If there are no services, do nothing
|
|
117
|
+
if (Object.entries(this.schema.services).length == 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generated comment
|
|
122
|
+
this.render.emitLine("// Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
123
|
+
this.render.ensureBlankLine();
|
|
124
|
+
|
|
125
|
+
// Import Nexus
|
|
126
|
+
this.render.emitLine('import * as nexus from "nexus-rpc";');
|
|
127
|
+
|
|
128
|
+
// Import all "existing" types
|
|
129
|
+
const existingTypesByFrom: Record<string, ExistingType[]> = {};
|
|
130
|
+
for (const existingType of Object.values(this.existingTypes)) {
|
|
131
|
+
if (existingType.from) {
|
|
132
|
+
if (!Object.hasOwn(existingTypesByFrom, existingType.from)) {
|
|
133
|
+
existingTypesByFrom[existingType.from] = [];
|
|
134
|
+
}
|
|
135
|
+
existingTypesByFrom[existingType.from].push(existingType);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// TODO(cretz): Better sorting?
|
|
139
|
+
for (const [from, existingTypes] of Object.entries(
|
|
140
|
+
existingTypesByFrom,
|
|
141
|
+
).toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
142
|
+
existingTypes.sort((a, b) => a.type.localeCompare(b.type));
|
|
143
|
+
const pieces = existingTypes.map((t) => {
|
|
144
|
+
let piece = `type ${t.type}`;
|
|
145
|
+
if (t.alias) piece += ` as ${t.alias}`;
|
|
146
|
+
return piece;
|
|
147
|
+
});
|
|
148
|
+
this.render.emitLine(
|
|
149
|
+
"import { ",
|
|
150
|
+
pieces.join(", "),
|
|
151
|
+
' } from "',
|
|
152
|
+
utf16StringEscape(from),
|
|
153
|
+
'";',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Emit each service
|
|
158
|
+
for (const [serviceName, serviceSchema] of Object.entries(
|
|
159
|
+
this.schema.services,
|
|
160
|
+
)) {
|
|
161
|
+
this.emitService(serviceName, serviceSchema);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
emitService(serviceName: string, serviceSchema: PreparedService) {
|
|
166
|
+
this.render.ensureBlankLine();
|
|
167
|
+
const constName = this.makeServiceTypeName(
|
|
168
|
+
this.render.nameStyle(serviceName, false),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
172
|
+
this.render.emitBlock(
|
|
173
|
+
[
|
|
174
|
+
"export const ",
|
|
175
|
+
constName,
|
|
176
|
+
' = nexus.service("',
|
|
177
|
+
utf16StringEscape(serviceName),
|
|
178
|
+
'", ',
|
|
179
|
+
],
|
|
180
|
+
");",
|
|
181
|
+
() => {
|
|
182
|
+
const propertyNamesInUse = {};
|
|
183
|
+
this.render.forEachWithBlankLines(
|
|
184
|
+
Object.entries(serviceSchema.operations),
|
|
185
|
+
"interposing",
|
|
186
|
+
(op, opName, pos) => {
|
|
187
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
188
|
+
const propertyName = this.makeOperationFunctionName(
|
|
189
|
+
this.render.nameStyle(opName, false),
|
|
190
|
+
propertyNamesInUse,
|
|
191
|
+
);
|
|
192
|
+
const opArguments =
|
|
193
|
+
opName == propertyName
|
|
194
|
+
? []
|
|
195
|
+
: ['{ name: "', utf16StringEscape(opName), '" }'];
|
|
196
|
+
this.render.emitLine(
|
|
197
|
+
propertyName,
|
|
198
|
+
": nexus.operation<",
|
|
199
|
+
this.getNexusType(op.input) ?? "void",
|
|
200
|
+
", ",
|
|
201
|
+
this.getNexusType(op.output) ?? "void",
|
|
202
|
+
">(",
|
|
203
|
+
opArguments,
|
|
204
|
+
"),",
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
emitTypes() {
|
|
213
|
+
// We cannot use original emitTypes because it renders all top level types
|
|
214
|
+
// as non-export and includes __ALL_TYPES__, but we want export and don't
|
|
215
|
+
// want that pseudo type. So we copy what Quicktype did mostly, with some
|
|
216
|
+
// alterations.
|
|
217
|
+
|
|
218
|
+
// Primitives
|
|
219
|
+
this.render.forEachWithBlankLines(
|
|
220
|
+
this.render.topLevels,
|
|
221
|
+
"none",
|
|
222
|
+
(t, name, pos) => {
|
|
223
|
+
if (!t.isPrimitive() || name == "__ALL_TYPES__") {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.render.ensureBlankLine();
|
|
228
|
+
this.render.emitDescription(this.render.descriptionForType(t));
|
|
229
|
+
this.render.emitLine(
|
|
230
|
+
"export type ",
|
|
231
|
+
name,
|
|
232
|
+
" = ",
|
|
233
|
+
this.render.sourceFor(t).source,
|
|
234
|
+
";",
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Named types
|
|
240
|
+
this.render.forEachNamedType(
|
|
241
|
+
"leading-and-interposing",
|
|
242
|
+
(c: ClassType, n: Name) => (this.render as any).emitClass(c, n),
|
|
243
|
+
(enm, n) => (this.render as any).emitEnum(enm, n),
|
|
244
|
+
(u, n) => (this.render as any).emitUnion(u, n),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
exportTopLevelPrimitives() {
|
|
249
|
+
// TypeScript rendered by default does not export primitive type aliases, so
|
|
250
|
+
// we must. First, collect set to export
|
|
251
|
+
const toExport = Object.keys(this.schema.topLevelJsonSchemaTypes).filter(
|
|
252
|
+
(name) => this.render.topLevels.get(name)?.isPrimitive(),
|
|
253
|
+
);
|
|
254
|
+
// Render if any
|
|
255
|
+
if (toExport.length > 0) {
|
|
256
|
+
this.render.ensureBlankLine();
|
|
257
|
+
this.render.emitLine("export { ", toExport.join(", "), " };");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getNexusType(
|
|
262
|
+
reference: PreparedTypeReference | undefined,
|
|
263
|
+
): Sourcelike | undefined {
|
|
264
|
+
if (!reference) {
|
|
265
|
+
return undefined;
|
|
266
|
+
} else if (reference.kind == "existing") {
|
|
267
|
+
const type = this.existingTypes[reference.name];
|
|
268
|
+
return type.alias ?? type.type;
|
|
269
|
+
} else {
|
|
270
|
+
const type = this.render.topLevels.get(reference.name);
|
|
271
|
+
if (!type) {
|
|
272
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// If the type is primitive, use the alias
|
|
276
|
+
if (type.isPrimitive()) {
|
|
277
|
+
return reference.name;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return this.render.sourceFor(type).source;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
opNameForbidden(name: string): boolean {
|
|
285
|
+
// We also want to forbid any Object functions
|
|
286
|
+
return super.opNameForbidden(name) || name in {};
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DefinitionSchema } from "./definition-schema.js";
|
|
2
|
+
|
|
3
|
+
import jsonSchema from "../../../../schemas/nexus-rpc-gen.json" with { type: "json" };
|
|
4
|
+
import Ajv from "ajv";
|
|
5
|
+
import addFormats from "ajv-formats";
|
|
6
|
+
import yaml from "yaml";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const ajv = new Ajv({
|
|
10
|
+
allErrors: true,
|
|
11
|
+
strict: false,
|
|
12
|
+
loadSchema: async (uri) => {
|
|
13
|
+
// TODO(cretz): This
|
|
14
|
+
throw new Error(`Unable to load remote schema at ${uri} at this time`);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
addFormats(ajv);
|
|
19
|
+
|
|
20
|
+
export async function parseFiles(files: string[]): Promise<DefinitionSchema> {
|
|
21
|
+
// TODO(cretz): Multi-file
|
|
22
|
+
if (files.length != 1) {
|
|
23
|
+
throw new Error("Must have only 1 file at this time");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Parse YAML
|
|
27
|
+
const document = yaml.parse(await readFile(files[0], "utf8"));
|
|
28
|
+
|
|
29
|
+
// Validate. We recreate the validator because it carries state.
|
|
30
|
+
const valueFunction = await ajv.compileAsync<DefinitionSchema>(jsonSchema);
|
|
31
|
+
if (!valueFunction(document)) {
|
|
32
|
+
for (const error of valueFunction.errors ?? []) {
|
|
33
|
+
console.log(error);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Found ${valueFunction.errors?.length} error(s): ` +
|
|
37
|
+
valueFunction.errors
|
|
38
|
+
?.map(
|
|
39
|
+
(error) =>
|
|
40
|
+
`${error.instancePath || "(root)"}: ${error.message} (${JSON.stringify(error.params)})`,
|
|
41
|
+
)
|
|
42
|
+
.join(", "),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return document;
|
|
47
|
+
}
|