@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,111 @@
|
|
|
1
|
+
import { javaOptions, JavaRenderer, JavaTargetLanguage, Namer, Type, } from "quicktype-core";
|
|
2
|
+
import { RenderAdapter } from "./render-adapter.js";
|
|
3
|
+
import { splitDescription } from "./utility.js";
|
|
4
|
+
import { stringEscape } from "quicktype-core/dist/language/Java/utils.js";
|
|
5
|
+
// Change some defaults globally
|
|
6
|
+
javaOptions.packageName.definition.defaultValue = "com.example.nexusservices";
|
|
7
|
+
export class JavaLanguageWithNexus extends JavaTargetLanguage {
|
|
8
|
+
get defaultIndentation() {
|
|
9
|
+
// We want two-space indent to be default for Java
|
|
10
|
+
return " ";
|
|
11
|
+
}
|
|
12
|
+
makeRenderer(renderContext, untypedOptionValues) {
|
|
13
|
+
const adapter = new JavaRenderAdapter(super.makeRenderer(renderContext, untypedOptionValues), untypedOptionValues);
|
|
14
|
+
return adapter.makeRenderer({
|
|
15
|
+
emitSourceStructure(original) {
|
|
16
|
+
adapter.emitServices();
|
|
17
|
+
original();
|
|
18
|
+
},
|
|
19
|
+
emitConverterClass(original) {
|
|
20
|
+
// No converter class wanted
|
|
21
|
+
},
|
|
22
|
+
forbiddenNamesForGlobalNamespace(original) {
|
|
23
|
+
const returnValue = original();
|
|
24
|
+
// We need to adjust some forbidden names to include Object base class items
|
|
25
|
+
returnValue.push("clone", "equals", "finalize", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait");
|
|
26
|
+
return returnValue;
|
|
27
|
+
},
|
|
28
|
+
startFile(original, basename) {
|
|
29
|
+
// Prepend a the package name for Java
|
|
30
|
+
const prepend = adapter.render._options.packageName.replaceAll(".", "/") + "/";
|
|
31
|
+
original([prepend, basename]);
|
|
32
|
+
// // Emit generated note
|
|
33
|
+
// adapter.render.emitLine("// Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
34
|
+
// adapter.render.ensureBlankLine();
|
|
35
|
+
},
|
|
36
|
+
emitPackageAndImports(original, imports) {
|
|
37
|
+
adapter.emitGeneratedComment();
|
|
38
|
+
original(imports);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class JavaRenderAdapter extends RenderAdapter {
|
|
44
|
+
emitServices() {
|
|
45
|
+
for (const [serviceName, serviceSchema] of Object.entries(this.schema.services)) {
|
|
46
|
+
this.emitService(serviceName, serviceSchema);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
emitService(serviceName, serviceSchema) {
|
|
50
|
+
// Collect imports
|
|
51
|
+
const imports = ["io.nexusrpc.Operation", "io.nexusrpc.Service"];
|
|
52
|
+
for (const [_, op] of Object.entries(serviceSchema.operations)) {
|
|
53
|
+
if (op.input?.kind == "jsonSchema") {
|
|
54
|
+
const type = this.render.topLevels.get(op.input.name);
|
|
55
|
+
if (type) {
|
|
56
|
+
imports.push(...this.render.javaImport(type));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (op.output?.kind == "jsonSchema") {
|
|
60
|
+
const type = this.render.topLevels.get(op.output.name);
|
|
61
|
+
if (type) {
|
|
62
|
+
imports.push(...this.render.javaImport(type));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Create class
|
|
67
|
+
// TODO(cretz): Research addNameForTopLevel and such to prevent service name clash
|
|
68
|
+
const className = this.makeServiceTypeName(this.render.makeNamedTypeNamer().nameStyle(serviceName));
|
|
69
|
+
const packagePrepend = this.render._options.packageName.replaceAll(".", "/") + "/";
|
|
70
|
+
this.emitGeneratedComment();
|
|
71
|
+
this.render.emitFileHeader([packagePrepend, className], imports);
|
|
72
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
73
|
+
this.render.emitLine("@Service", className == serviceName
|
|
74
|
+
? []
|
|
75
|
+
: ['(name="', stringEscape(serviceName), '")']);
|
|
76
|
+
const methodNamesInUse = {};
|
|
77
|
+
this.render.emitBlock(["public interface ", className], () => {
|
|
78
|
+
this.render.forEachWithBlankLines(Object.entries(serviceSchema.operations), "interposing", (op, opName, pos) => {
|
|
79
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
80
|
+
const methodName = this.makeOperationFunctionName(this.render.namerForObjectProperty().nameStyle(opName), methodNamesInUse);
|
|
81
|
+
this.render.emitLine("@Operation", methodName == opName ? [] : ['(name="', stringEscape(opName), '")']);
|
|
82
|
+
const inType = this.getNexusType(op.input);
|
|
83
|
+
this.render.emitLine(this.getNexusType(op.output) ?? "void", " ", methodName, "(", inType ? [inType, " input"] : [], ");");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
this.render.finishFile();
|
|
87
|
+
}
|
|
88
|
+
emitGeneratedComment() {
|
|
89
|
+
this.render.emitLine("// Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
90
|
+
this.render.ensureBlankLine();
|
|
91
|
+
}
|
|
92
|
+
emitPackageAndImports(imports) {
|
|
93
|
+
this.emitGeneratedComment();
|
|
94
|
+
this.render.emitPackageAndImports(imports);
|
|
95
|
+
}
|
|
96
|
+
getNexusType(reference) {
|
|
97
|
+
if (!reference) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
else if (reference.kind == "existing") {
|
|
101
|
+
return reference.name;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const type = this.render.topLevels.get(reference.name);
|
|
105
|
+
if (!type) {
|
|
106
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
107
|
+
}
|
|
108
|
+
return this.render.javaType(false, type);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { PythonRenderer, PythonTargetLanguage, type LanguageName, type RenderContext, type RendererOptions } from "quicktype-core";
|
|
2
|
+
export declare class PythonLanguageWithNexus extends PythonTargetLanguage {
|
|
3
|
+
protected makeRenderer<Lang extends LanguageName = "python">(renderContext: RenderContext, untypedOptionValues: RendererOptions<Lang>): PythonRenderer;
|
|
4
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ClassType, ConvenienceRenderer, Name, Namer, pythonOptions, PythonRenderer, PythonTargetLanguage, Type, } from "quicktype-core";
|
|
2
|
+
import { RenderAdapter } from "./render-adapter.js";
|
|
3
|
+
import { splitDescription } from "./utility.js";
|
|
4
|
+
// Change some defaults globally
|
|
5
|
+
pythonOptions.justTypes.definition.defaultValue = true;
|
|
6
|
+
pythonOptions.features.definition.defaultValue = "3.7";
|
|
7
|
+
pythonOptions.nicePropertyNames.definition.defaultValue = true;
|
|
8
|
+
pythonOptions.pydanticBaseModel.definition.defaultValue = true;
|
|
9
|
+
export class PythonLanguageWithNexus extends PythonTargetLanguage {
|
|
10
|
+
makeRenderer(renderContext, untypedOptionValues) {
|
|
11
|
+
const adapter = new PythonRenderAdapter(super.makeRenderer(renderContext, untypedOptionValues), untypedOptionValues);
|
|
12
|
+
adapter.assertValidOptions();
|
|
13
|
+
return adapter.makeRenderer({
|
|
14
|
+
emitSourceStructure(original, givenOutputFilename) {
|
|
15
|
+
// Generated comment
|
|
16
|
+
adapter.render.emitLine("# Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
17
|
+
adapter.render.ensureBlankLine();
|
|
18
|
+
// We need to add the future annotation
|
|
19
|
+
// TODO(cretz): Have option to remove this?
|
|
20
|
+
adapter.render.emitLine("from __future__ import annotations");
|
|
21
|
+
adapter.render.ensureBlankLine();
|
|
22
|
+
original(givenOutputFilename);
|
|
23
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
24
|
+
},
|
|
25
|
+
emitClosingCode(original) {
|
|
26
|
+
original();
|
|
27
|
+
// Emit services _after_ all types are present. We choose to be after
|
|
28
|
+
// the model types so we don't have any "ForwardRef"s to any models
|
|
29
|
+
// which is not supported by Nexus Python's handler type-checking.
|
|
30
|
+
adapter.emitServices();
|
|
31
|
+
},
|
|
32
|
+
emitClass(_original, t) {
|
|
33
|
+
adapter.emitClass(t);
|
|
34
|
+
},
|
|
35
|
+
emitImports(original) {
|
|
36
|
+
original();
|
|
37
|
+
adapter.emitAdditionalImports();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const emptyNameMap = new Map();
|
|
43
|
+
class PythonRenderAdapter extends RenderAdapter {
|
|
44
|
+
typeNamer;
|
|
45
|
+
propertyNamer;
|
|
46
|
+
constructor(render, rendererOptions) {
|
|
47
|
+
super(render, rendererOptions);
|
|
48
|
+
this.typeNamer = this.render.makeNamedTypeNamer();
|
|
49
|
+
this.propertyNamer = this.render.namerForObjectProperty();
|
|
50
|
+
}
|
|
51
|
+
assertValidOptions() {
|
|
52
|
+
// Currently, we only support specific Python options
|
|
53
|
+
if (!this.render.pyOptions.justTypes) {
|
|
54
|
+
throw new Error("Python option for just-types must be set");
|
|
55
|
+
}
|
|
56
|
+
if (!this.render.pyOptions.pydanticBaseModel) {
|
|
57
|
+
throw new Error("Python option for pydantic-model must be set");
|
|
58
|
+
}
|
|
59
|
+
if (!this.render.pyOptions.features.typeHints) {
|
|
60
|
+
throw new Error("Python option to include type hints must be set");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
makeFileName() {
|
|
64
|
+
// If there is a single service, use that, otherwise use the
|
|
65
|
+
// filename sans extensions to build it
|
|
66
|
+
const services = Object.entries(this.schema.services);
|
|
67
|
+
const name = services.length == 1
|
|
68
|
+
? services[0][0]
|
|
69
|
+
: this.nexusRendererOptions.firstFilenameSansExtensions;
|
|
70
|
+
return this.propertyNamer.nameStyle(name) + ".py";
|
|
71
|
+
}
|
|
72
|
+
emitAdditionalImports() {
|
|
73
|
+
// We have to emit imports for existing types with dots. Unlike the existing
|
|
74
|
+
// withImport/emitImports, we do not want from X import Y, we want just
|
|
75
|
+
// import and we will fully-qualify the types at the usage site.
|
|
76
|
+
const toImport = [];
|
|
77
|
+
for (const svcSchema of Object.values(this.schema.services)) {
|
|
78
|
+
for (const opSchema of Object.values(svcSchema.operations)) {
|
|
79
|
+
for (const type of [opSchema.input, opSchema.output]) {
|
|
80
|
+
if (type?.kind == "existing") {
|
|
81
|
+
const lastDot = type.name.lastIndexOf(".");
|
|
82
|
+
if (lastDot >= 0 &&
|
|
83
|
+
!toImport.includes(type.name.slice(0, lastDot))) {
|
|
84
|
+
toImport.push(type.name.slice(0, lastDot));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
toImport.sort();
|
|
91
|
+
for (const module_ of toImport) {
|
|
92
|
+
this.render.emitLine("import ", module_);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
emitServices() {
|
|
96
|
+
for (const [serviceName, serviceSchema] of Object.entries(this.schema.services)) {
|
|
97
|
+
this.emitService(serviceName, serviceSchema);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
emitService(serviceName, serviceSchema) {
|
|
101
|
+
this.render.ensureBlankLine(2);
|
|
102
|
+
const typeName = this.makeServiceTypeName(this.typeNamer.nameStyle(serviceName));
|
|
103
|
+
// Decorator
|
|
104
|
+
this.render.emitLine("@", this.render.withImport("nexusrpc", "service"), serviceName == typeName
|
|
105
|
+
? []
|
|
106
|
+
: ["(name=", this.render.string(serviceName), ")"]);
|
|
107
|
+
// Service class with each property
|
|
108
|
+
this.render.emitBlock(["class ", typeName, ":"], () => {
|
|
109
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
110
|
+
if (Object.entries(serviceSchema.operations).length == 0) {
|
|
111
|
+
this.render.emitLine("pass");
|
|
112
|
+
}
|
|
113
|
+
const propertyNamesInUse = {};
|
|
114
|
+
this.render.forEachWithBlankLines(Object.entries(serviceSchema.operations), "interposing", (op, opName, pos) => {
|
|
115
|
+
const propertyName = this.makeOperationFunctionName(this.propertyNamer.nameStyle(opName), propertyNamesInUse);
|
|
116
|
+
this.render.emitLine(propertyName, ": ", this.render.withImport("nexusrpc", "Operation"), "[", this.getNexusType(op.input) ?? "None", ", ", this.getNexusType(op.output) ?? "None", "]", opName == propertyName
|
|
117
|
+
? []
|
|
118
|
+
: [
|
|
119
|
+
" = ",
|
|
120
|
+
this.render.withImport("nexusrpc", "Operation"),
|
|
121
|
+
"(name=",
|
|
122
|
+
this.render.string(opName),
|
|
123
|
+
")",
|
|
124
|
+
]);
|
|
125
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
emitClass(t) {
|
|
130
|
+
this.render.declareType(t, () => {
|
|
131
|
+
if (t.getProperties().size === 0) {
|
|
132
|
+
this.render.emitLine("pass");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.render.forEachClassProperty(t, "none", (name, jsonName, cp) => {
|
|
136
|
+
// Get the type string and append a pydantic Field to it if the name
|
|
137
|
+
// doesn't match the JSON name.
|
|
138
|
+
let typeSource = this.render.pythonType(cp.type, true);
|
|
139
|
+
// const fieldName = name.namingFunction.nameStyle(name.firstProposedName(emptyNameMap));
|
|
140
|
+
const fieldName = this.render.sourcelikeToString(name);
|
|
141
|
+
if (fieldName != jsonName) {
|
|
142
|
+
// Ellipsis means no default. In optional situations, typeSource has a
|
|
143
|
+
// trailing " = None" which we need to remove and set as default
|
|
144
|
+
let fieldDefault = "...";
|
|
145
|
+
if (Array.isArray(typeSource) && typeSource.at(-1) == " = None") {
|
|
146
|
+
typeSource = typeSource.slice(0, -1);
|
|
147
|
+
fieldDefault = "None";
|
|
148
|
+
}
|
|
149
|
+
typeSource = [
|
|
150
|
+
typeSource,
|
|
151
|
+
" = ",
|
|
152
|
+
this.render.withImport("pydantic", "Field"),
|
|
153
|
+
"(",
|
|
154
|
+
fieldDefault,
|
|
155
|
+
", serialization_alias=",
|
|
156
|
+
this.render.string(jsonName),
|
|
157
|
+
")",
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
this.render.emitLine(name, ": ", typeSource);
|
|
161
|
+
this.render.emitDescription(this.render.descriptionForClassProperty(t, jsonName));
|
|
162
|
+
});
|
|
163
|
+
this.render.ensureBlankLine();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
getNexusType(reference) {
|
|
167
|
+
if (!reference) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
else if (reference.kind == "existing") {
|
|
171
|
+
return reference.name;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const type = this.render.topLevels.get(reference.name);
|
|
175
|
+
if (!type) {
|
|
176
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
177
|
+
}
|
|
178
|
+
return this.render.pythonType(type, false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { TypeScriptRenderer, TypeScriptTargetLanguage, type LanguageName, type RenderContext, type RendererOptions } from "quicktype-core";
|
|
2
|
+
export declare class TypeScriptLanguageWithNexus extends TypeScriptTargetLanguage {
|
|
3
|
+
protected get defaultIndentation(): string;
|
|
4
|
+
protected makeRenderer<Lang extends LanguageName = "typescript">(renderContext: RenderContext, untypedOptionValues: RendererOptions<Lang>): TypeScriptRenderer;
|
|
5
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ClassType, Name, tsFlowOptions, Type, TypeScriptRenderer, TypeScriptTargetLanguage, } from "quicktype-core";
|
|
2
|
+
import { RenderAdapter } from "./render-adapter.js";
|
|
3
|
+
import { splitDescription } from "./utility.js";
|
|
4
|
+
import { utf16StringEscape } from "quicktype-core/dist/support/Strings.js";
|
|
5
|
+
// Change some defaults globally
|
|
6
|
+
tsFlowOptions.justTypes.definition.defaultValue = true;
|
|
7
|
+
export class TypeScriptLanguageWithNexus extends TypeScriptTargetLanguage {
|
|
8
|
+
get defaultIndentation() {
|
|
9
|
+
// We want two-space indent to be default for TypeScript
|
|
10
|
+
return " ";
|
|
11
|
+
}
|
|
12
|
+
makeRenderer(renderContext, untypedOptionValues) {
|
|
13
|
+
const adapter = new TypeScriptRenderAdapter(super.makeRenderer(renderContext, untypedOptionValues), untypedOptionValues);
|
|
14
|
+
return adapter.makeRenderer({
|
|
15
|
+
emitSourceStructure(original) {
|
|
16
|
+
adapter.emitServices();
|
|
17
|
+
original();
|
|
18
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
19
|
+
},
|
|
20
|
+
emitTypes(original) {
|
|
21
|
+
// We cannot use original emitTypes, see override for reason why
|
|
22
|
+
adapter.emitTypes();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class TypeScriptRenderAdapter extends RenderAdapter {
|
|
28
|
+
_existingTypes;
|
|
29
|
+
makeFileName() {
|
|
30
|
+
// If there is a single service, use that, otherwise use the
|
|
31
|
+
// filename sans extensions to build it
|
|
32
|
+
const services = Object.entries(this.schema.services);
|
|
33
|
+
if (services.length == 1) {
|
|
34
|
+
return `${services[0][0]}.ts`;
|
|
35
|
+
}
|
|
36
|
+
return `${this.nexusRendererOptions.firstFilenameSansExtensions}.ts`;
|
|
37
|
+
}
|
|
38
|
+
// Key is full string as given in schema
|
|
39
|
+
get existingTypes() {
|
|
40
|
+
if (this._existingTypes === undefined) {
|
|
41
|
+
this._existingTypes = {};
|
|
42
|
+
const inUse = {};
|
|
43
|
+
for (const serviceSchema of Object.values(this.schema.services)) {
|
|
44
|
+
for (const opSchema of Object.values(serviceSchema.operations)) {
|
|
45
|
+
for (const type of [opSchema.input, opSchema.output]) {
|
|
46
|
+
if (type?.kind == "existing") {
|
|
47
|
+
// TODO(cretz): Generics with qualified type args that need to be imported?
|
|
48
|
+
const lastHash = type.name.lastIndexOf("#");
|
|
49
|
+
const existingType = {
|
|
50
|
+
type: type.name.slice(lastHash + 1),
|
|
51
|
+
from: lastHash == -1 ? undefined : type.name.slice(0, lastHash),
|
|
52
|
+
};
|
|
53
|
+
// Alias it while it already exists
|
|
54
|
+
let alias = existingType.type;
|
|
55
|
+
for (let index = 1; Object.hasOwn(inUse, alias) ||
|
|
56
|
+
this.topLevelNameInUse(alias, true); index++) {
|
|
57
|
+
alias = `${existingType.type}${index}`;
|
|
58
|
+
}
|
|
59
|
+
if (alias != existingType.type) {
|
|
60
|
+
existingType.alias = alias;
|
|
61
|
+
}
|
|
62
|
+
this._existingTypes[type.name] = existingType;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return this._existingTypes;
|
|
69
|
+
}
|
|
70
|
+
emitServices() {
|
|
71
|
+
// If there are no services, do nothing
|
|
72
|
+
if (Object.entries(this.schema.services).length == 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Generated comment
|
|
76
|
+
this.render.emitLine("// Generated by nexus-rpc-gen. DO NOT EDIT!");
|
|
77
|
+
this.render.ensureBlankLine();
|
|
78
|
+
// Import Nexus
|
|
79
|
+
this.render.emitLine('import * as nexus from "nexus-rpc";');
|
|
80
|
+
// Import all "existing" types
|
|
81
|
+
const existingTypesByFrom = {};
|
|
82
|
+
for (const existingType of Object.values(this.existingTypes)) {
|
|
83
|
+
if (existingType.from) {
|
|
84
|
+
if (!Object.hasOwn(existingTypesByFrom, existingType.from)) {
|
|
85
|
+
existingTypesByFrom[existingType.from] = [];
|
|
86
|
+
}
|
|
87
|
+
existingTypesByFrom[existingType.from].push(existingType);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// TODO(cretz): Better sorting?
|
|
91
|
+
for (const [from, existingTypes] of Object.entries(existingTypesByFrom).toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
92
|
+
existingTypes.sort((a, b) => a.type.localeCompare(b.type));
|
|
93
|
+
const pieces = existingTypes.map((t) => {
|
|
94
|
+
let piece = `type ${t.type}`;
|
|
95
|
+
if (t.alias)
|
|
96
|
+
piece += ` as ${t.alias}`;
|
|
97
|
+
return piece;
|
|
98
|
+
});
|
|
99
|
+
this.render.emitLine("import { ", pieces.join(", "), ' } from "', utf16StringEscape(from), '";');
|
|
100
|
+
}
|
|
101
|
+
// Emit each service
|
|
102
|
+
for (const [serviceName, serviceSchema] of Object.entries(this.schema.services)) {
|
|
103
|
+
this.emitService(serviceName, serviceSchema);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
emitService(serviceName, serviceSchema) {
|
|
107
|
+
this.render.ensureBlankLine();
|
|
108
|
+
const constName = this.makeServiceTypeName(this.render.nameStyle(serviceName, false));
|
|
109
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
110
|
+
this.render.emitBlock([
|
|
111
|
+
"export const ",
|
|
112
|
+
constName,
|
|
113
|
+
' = nexus.service("',
|
|
114
|
+
utf16StringEscape(serviceName),
|
|
115
|
+
'", ',
|
|
116
|
+
], ");", () => {
|
|
117
|
+
const propertyNamesInUse = {};
|
|
118
|
+
this.render.forEachWithBlankLines(Object.entries(serviceSchema.operations), "interposing", (op, opName, pos) => {
|
|
119
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
120
|
+
const propertyName = this.makeOperationFunctionName(this.render.nameStyle(opName, false), propertyNamesInUse);
|
|
121
|
+
const opArguments = opName == propertyName
|
|
122
|
+
? []
|
|
123
|
+
: ['{ name: "', utf16StringEscape(opName), '" }'];
|
|
124
|
+
this.render.emitLine(propertyName, ": nexus.operation<", this.getNexusType(op.input) ?? "void", ", ", this.getNexusType(op.output) ?? "void", ">(", opArguments, "),");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
emitTypes() {
|
|
129
|
+
// We cannot use original emitTypes because it renders all top level types
|
|
130
|
+
// as non-export and includes __ALL_TYPES__, but we want export and don't
|
|
131
|
+
// want that pseudo type. So we copy what Quicktype did mostly, with some
|
|
132
|
+
// alterations.
|
|
133
|
+
// Primitives
|
|
134
|
+
this.render.forEachWithBlankLines(this.render.topLevels, "none", (t, name, pos) => {
|
|
135
|
+
if (!t.isPrimitive() || name == "__ALL_TYPES__") {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.render.ensureBlankLine();
|
|
139
|
+
this.render.emitDescription(this.render.descriptionForType(t));
|
|
140
|
+
this.render.emitLine("export type ", name, " = ", this.render.sourceFor(t).source, ";");
|
|
141
|
+
});
|
|
142
|
+
// Named types
|
|
143
|
+
this.render.forEachNamedType("leading-and-interposing", (c, n) => this.render.emitClass(c, n), (enm, n) => this.render.emitEnum(enm, n), (u, n) => this.render.emitUnion(u, n));
|
|
144
|
+
}
|
|
145
|
+
exportTopLevelPrimitives() {
|
|
146
|
+
// TypeScript rendered by default does not export primitive type aliases, so
|
|
147
|
+
// we must. First, collect set to export
|
|
148
|
+
const toExport = Object.keys(this.schema.topLevelJsonSchemaTypes).filter((name) => this.render.topLevels.get(name)?.isPrimitive());
|
|
149
|
+
// Render if any
|
|
150
|
+
if (toExport.length > 0) {
|
|
151
|
+
this.render.ensureBlankLine();
|
|
152
|
+
this.render.emitLine("export { ", toExport.join(", "), " };");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
getNexusType(reference) {
|
|
156
|
+
if (!reference) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
else if (reference.kind == "existing") {
|
|
160
|
+
const type = this.existingTypes[reference.name];
|
|
161
|
+
return type.alias ?? type.type;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const type = this.render.topLevels.get(reference.name);
|
|
165
|
+
if (!type) {
|
|
166
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
167
|
+
}
|
|
168
|
+
// If the type is primitive, use the alias
|
|
169
|
+
if (type.isPrimitive()) {
|
|
170
|
+
return reference.name;
|
|
171
|
+
}
|
|
172
|
+
return this.render.sourceFor(type).source;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
opNameForbidden(name) {
|
|
176
|
+
// We also want to forbid any Object functions
|
|
177
|
+
return super.opNameForbidden(name) || name in {};
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/parser.d.ts
ADDED
package/dist/parser.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import jsonSchema from "../../../../schemas/nexus-rpc-gen.json" with { type: "json" };
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
const ajv = new Ajv({
|
|
7
|
+
allErrors: true,
|
|
8
|
+
strict: false,
|
|
9
|
+
loadSchema: async (uri) => {
|
|
10
|
+
// TODO(cretz): This
|
|
11
|
+
throw new Error(`Unable to load remote schema at ${uri} at this time`);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
addFormats(ajv);
|
|
15
|
+
export async function parseFiles(files) {
|
|
16
|
+
// TODO(cretz): Multi-file
|
|
17
|
+
if (files.length != 1) {
|
|
18
|
+
throw new Error("Must have only 1 file at this time");
|
|
19
|
+
}
|
|
20
|
+
// Parse YAML
|
|
21
|
+
const document = yaml.parse(await readFile(files[0], "utf8"));
|
|
22
|
+
// Validate. We recreate the validator because it carries state.
|
|
23
|
+
const valueFunction = await ajv.compileAsync(jsonSchema);
|
|
24
|
+
if (!valueFunction(document)) {
|
|
25
|
+
for (const error of valueFunction.errors ?? []) {
|
|
26
|
+
console.log(error);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Found ${valueFunction.errors?.length} error(s): ` +
|
|
29
|
+
valueFunction.errors
|
|
30
|
+
?.map((error) => `${error.instancePath || "(root)"}: ${error.message} (${JSON.stringify(error.params)})`)
|
|
31
|
+
.join(", "));
|
|
32
|
+
}
|
|
33
|
+
return document;
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ConvenienceRenderer, Name, Namer, type RendererOptions, type Sourcelike } from "quicktype-core";
|
|
2
|
+
import { type NexusRendererOptions, type PreparedSchema } from "./generator.js";
|
|
3
|
+
import type { BlankLineConfig, ForEachPosition } from "quicktype-core/dist/Renderer.js";
|
|
4
|
+
import type { ClassProperty, ClassType, EnumType, ObjectType, Type, TypeGraph, UnionType } from "quicktype-core/dist/Type/index.js";
|
|
5
|
+
import type { ForbiddenWordsInfo } from "quicktype-core/dist/ConvenienceRenderer.js";
|
|
6
|
+
type AnyFunction = (...arguments_: any[]) => any;
|
|
7
|
+
export declare abstract class RenderAdapter<AccessibleRenderer> {
|
|
8
|
+
readonly render: AccessibleRenderer & RenderAccessible & ConvenienceRenderer;
|
|
9
|
+
protected readonly nexusRendererOptions: NexusRendererOptions;
|
|
10
|
+
private readonly serviceTypeNamesInUse;
|
|
11
|
+
constructor(render: ConvenienceRenderer, rendererOptions: RendererOptions);
|
|
12
|
+
get schema(): PreparedSchema;
|
|
13
|
+
makeRenderer<T extends ConvenienceRenderer>(overrides: {
|
|
14
|
+
[K in keyof AccessibleRenderer]?: AccessibleRenderer[K] extends AnyFunction ? (this: AccessibleRenderer, original: AccessibleRenderer[K], ...arguments_: Parameters<AccessibleRenderer[K]>) => ReturnType<AccessibleRenderer[K]> : never;
|
|
15
|
+
}): T;
|
|
16
|
+
makeServiceTypeName(idealName: string): string;
|
|
17
|
+
makeOperationFunctionName(idealName: string, inUse: Record<string, boolean>): string;
|
|
18
|
+
topLevelNameInUse(name: string, includeServices: boolean): boolean;
|
|
19
|
+
opNameForbidden(name: string): boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface RenderAccessible {
|
|
22
|
+
readonly typeGraph: TypeGraph;
|
|
23
|
+
descriptionForClassProperty(o: ObjectType, name: string): string[] | undefined;
|
|
24
|
+
descriptionForType(t: Type): string[] | undefined;
|
|
25
|
+
emitDescription(description: Sourcelike[] | undefined): void;
|
|
26
|
+
emitGatheredSource(items: Sourcelike[]): void;
|
|
27
|
+
emitLineOnce(...lineParts: Sourcelike[]): void;
|
|
28
|
+
emitSourceStructure(givenOutputFilename: string): void;
|
|
29
|
+
emitTable(tableArray: Sourcelike[][]): void;
|
|
30
|
+
finishFile(filename: string): void;
|
|
31
|
+
forbiddenForObjectProperties(_o: ObjectType, _className: Name): ForbiddenWordsInfo;
|
|
32
|
+
forbiddenNamesForGlobalNamespace(): readonly string[];
|
|
33
|
+
forEachClassProperty(o: ObjectType, blankLocations: BlankLineConfig, f: (name: Name, jsonName: string, p: ClassProperty, position: ForEachPosition) => void): void;
|
|
34
|
+
forEachNamedType(blankLocations: BlankLineConfig, objectFunction: ((c: ClassType, className: Name, position: ForEachPosition) => void) | ((o: ObjectType, objectName: Name, position: ForEachPosition) => void), enumFunction: (enm: EnumType, enumName: Name, position: ForEachPosition) => void, unionFunction: (u: UnionType, unionName: Name, position: ForEachPosition) => void): void;
|
|
35
|
+
forEachTopLevel(blankLocations: BlankLineConfig, f: (t: Type, name: Name, position: ForEachPosition) => void, predicate?: (t: Type) => boolean): boolean;
|
|
36
|
+
forEachWithBlankLines<K, V>(iterable: Iterable<[K, V]>, blankLineConfig: BlankLineConfig, emitter: (v: V, k: K, position: ForEachPosition) => void): boolean;
|
|
37
|
+
gatherSource(emitter: () => void): Sourcelike[];
|
|
38
|
+
makeNamedTypeNamer(): Namer;
|
|
39
|
+
makeNameForTopLevel(_t: Type, givenName: string, _maybeNamedType: Type | undefined): Name;
|
|
40
|
+
nameForNamedType(t: Type): Name;
|
|
41
|
+
namerForObjectProperty(o: ObjectType, p: ClassProperty): Namer | null;
|
|
42
|
+
sourcelikeToString(source: Sourcelike): string;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ConvenienceRenderer, Name, Namer, } from "quicktype-core";
|
|
2
|
+
import { getNexusRendererOptions, } from "./generator.js";
|
|
3
|
+
import { proxyWithOverrides } from "./utility.js";
|
|
4
|
+
import { FixedName } from "quicktype-core/dist/Naming.js";
|
|
5
|
+
export class RenderAdapter {
|
|
6
|
+
render;
|
|
7
|
+
nexusRendererOptions;
|
|
8
|
+
serviceTypeNamesInUse = {};
|
|
9
|
+
constructor(render, rendererOptions) {
|
|
10
|
+
this.render = render;
|
|
11
|
+
this.nexusRendererOptions = getNexusRendererOptions(rendererOptions);
|
|
12
|
+
}
|
|
13
|
+
get schema() {
|
|
14
|
+
return this.nexusRendererOptions.nexusSchema;
|
|
15
|
+
}
|
|
16
|
+
makeRenderer(overrides) {
|
|
17
|
+
return proxyWithOverrides(this.render, overrides);
|
|
18
|
+
}
|
|
19
|
+
makeServiceTypeName(idealName) {
|
|
20
|
+
// If name in use but not with services, suffix "Service"
|
|
21
|
+
if (this.topLevelNameInUse(idealName, false)) {
|
|
22
|
+
idealName += "Service";
|
|
23
|
+
}
|
|
24
|
+
// If name in use, append number, starting with 2
|
|
25
|
+
if (this.topLevelNameInUse(idealName, true)) {
|
|
26
|
+
for (let index = 2;; index++) {
|
|
27
|
+
if (!this.topLevelNameInUse(`${idealName}${index}`, true)) {
|
|
28
|
+
idealName += `${index}`;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.serviceTypeNamesInUse[idealName] = true;
|
|
34
|
+
return idealName;
|
|
35
|
+
}
|
|
36
|
+
makeOperationFunctionName(idealName, inUse) {
|
|
37
|
+
// If forbidden, suffix with "Operation"
|
|
38
|
+
if (this.opNameForbidden(idealName)) {
|
|
39
|
+
idealName += "Operation";
|
|
40
|
+
}
|
|
41
|
+
// Append numbers starting with 2 if already in use
|
|
42
|
+
if (Object.hasOwn(inUse, idealName)) {
|
|
43
|
+
for (let index = 2;; index++) {
|
|
44
|
+
if (!Object.hasOwn(inUse, `${idealName}${index}`)) {
|
|
45
|
+
idealName += `${index}`;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
inUse[idealName] = true;
|
|
51
|
+
return idealName;
|
|
52
|
+
}
|
|
53
|
+
topLevelNameInUse(name, includeServices) {
|
|
54
|
+
// Considered in use if forbidden
|
|
55
|
+
if (this.render.forbiddenNamesForGlobalNamespace().includes(name)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// Check services
|
|
59
|
+
if (includeServices && Object.hasOwn(this.serviceTypeNamesInUse, name)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
let inNames = false;
|
|
63
|
+
this.render.names.forEach((value) => {
|
|
64
|
+
if (value == name) {
|
|
65
|
+
inNames = true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return inNames;
|
|
69
|
+
}
|
|
70
|
+
opNameForbidden(name) {
|
|
71
|
+
// TODO(cretz): hacking undefined in for object type ok?
|
|
72
|
+
const info = this.render.forbiddenForObjectProperties(undefined, new FixedName(name));
|
|
73
|
+
if (info.includeGlobalForbidden && this.topLevelNameInUse(name, true)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
for (const badName of info.names) {
|
|
77
|
+
if (badName instanceof Name) {
|
|
78
|
+
if (name == this.render.names.get(badName)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (name == badName) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type AnyFunction = (...arguments_: any[]) => any;
|
|
2
|
+
/**
|
|
3
|
+
* A helper that proxies an instance and overrides selected methods.
|
|
4
|
+
* The overrides receive both the original method and its typed parameters.
|
|
5
|
+
*/
|
|
6
|
+
export declare function proxyWithOverrides<T extends object>(instance: T, overrides: {
|
|
7
|
+
[K in keyof T]?: T[K] extends AnyFunction ? (this: T, original: T[K], ...arguments_: Parameters<T[K]>) => ReturnType<T[K]> : never;
|
|
8
|
+
}): T;
|
|
9
|
+
export declare function splitDescription(string_: string | undefined): string[] | undefined;
|
|
10
|
+
export {};
|