@nexus-rpc/gen-core 0.1.0-alpha.3
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/LICENSE +21 -0
- package/README.md +5 -0
- 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 +36 -0
- package/schemas/nexus-rpc-gen.json +98 -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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Temporal Technologies Inc. All Rights Reserved
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @nexus-rpc/gen-core
|
|
2
|
+
|
|
3
|
+
Core library for Nexus RPC code generation. This package provides the parsing, generation, and language-specific rendering logic used by the Nexus RPC code generator.
|
|
4
|
+
|
|
5
|
+
For installation, usage, and detailed documentation, see the [main repository README](https://github.com/nexus-rpc/nexus-rpc-gen/blob/main/README.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition for Nexus RPC services and operations
|
|
3
|
+
*/
|
|
4
|
+
export interface DefinitionSchema {
|
|
5
|
+
nexusrpc: string;
|
|
6
|
+
services?: {
|
|
7
|
+
[key: string]: ServiceValue;
|
|
8
|
+
};
|
|
9
|
+
types?: {
|
|
10
|
+
[key: string]: {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ServiceValue {
|
|
16
|
+
description?: string;
|
|
17
|
+
operations: {
|
|
18
|
+
[key: string]: OperationValue;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface OperationValue {
|
|
22
|
+
description?: string;
|
|
23
|
+
input?: {
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
};
|
|
26
|
+
output?: {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type LanguageName, type RendererOptions, type TargetLanguage } from "quicktype-core";
|
|
2
|
+
import type { DefinitionSchema } from "./definition-schema";
|
|
3
|
+
export interface GeneratorOptions<Lang extends LanguageName = LanguageName> {
|
|
4
|
+
lang: TargetLanguage;
|
|
5
|
+
schema: DefinitionSchema;
|
|
6
|
+
rendererOptions: RendererOptions<Lang>;
|
|
7
|
+
firstFilenameSansExtensions: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PreparedSchema {
|
|
10
|
+
services: {
|
|
11
|
+
[key: string]: PreparedService;
|
|
12
|
+
};
|
|
13
|
+
sharedJsonSchema: {
|
|
14
|
+
types: {
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
topLevelJsonSchemaTypes: {
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
};
|
|
21
|
+
topLevelJsonSchemaLocalRefs: {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface PreparedService {
|
|
26
|
+
description?: string;
|
|
27
|
+
operations: {
|
|
28
|
+
[key: string]: PreparedOperation;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface PreparedOperation {
|
|
32
|
+
description?: string;
|
|
33
|
+
input?: PreparedTypeReference;
|
|
34
|
+
output?: PreparedTypeReference;
|
|
35
|
+
}
|
|
36
|
+
export interface PreparedTypeReference {
|
|
37
|
+
kind: "jsonSchema" | "existing";
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
export interface NexusRendererOptions {
|
|
41
|
+
nexusSchema: PreparedSchema;
|
|
42
|
+
firstFilenameSansExtensions: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function getNexusRendererOptions(rendererOptions: RendererOptions): NexusRendererOptions;
|
|
45
|
+
export declare class Generator {
|
|
46
|
+
private readonly options;
|
|
47
|
+
constructor(options: GeneratorOptions);
|
|
48
|
+
generate(): Promise<{
|
|
49
|
+
[fileName: string]: string;
|
|
50
|
+
}>;
|
|
51
|
+
private prepareSchema;
|
|
52
|
+
private prepareInOutType;
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { InputData, JSONSchemaInput, JSONSchemaStore, quicktypeMultiFile, Ref, } from "quicktype-core";
|
|
2
|
+
import { PathElementKind } from "quicktype-core/dist/input/PathElement.js";
|
|
3
|
+
import { isPrimitiveTypeKind } from "quicktype-core/dist/Type/index.js";
|
|
4
|
+
export function getNexusRendererOptions(rendererOptions) {
|
|
5
|
+
return rendererOptions.nexusOptions;
|
|
6
|
+
}
|
|
7
|
+
export class Generator {
|
|
8
|
+
options;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
async generate() {
|
|
13
|
+
// Prepare schema
|
|
14
|
+
const schema = this.prepareSchema();
|
|
15
|
+
// Build quicktype input
|
|
16
|
+
const schemaInput = new JSONSchemaInput(new FetchDisabledSchemaStore());
|
|
17
|
+
const jsonSchema = {
|
|
18
|
+
...schema.sharedJsonSchema,
|
|
19
|
+
...schema.topLevelJsonSchemaTypes,
|
|
20
|
+
};
|
|
21
|
+
await schemaInput.addSource({
|
|
22
|
+
// TODO(cretz): Give proper filename name here for proper cross-file referencing
|
|
23
|
+
name: "__ALL_TYPES__",
|
|
24
|
+
schema: JSON.stringify(jsonSchema),
|
|
25
|
+
});
|
|
26
|
+
// Set the top-level types
|
|
27
|
+
for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
|
|
28
|
+
schemaInput.addTopLevel(topLevel, Ref.parse(`__ALL_TYPES__#/${topLevel}`));
|
|
29
|
+
}
|
|
30
|
+
for (const [name, reference] of Object.entries(schema.topLevelJsonSchemaLocalRefs)) {
|
|
31
|
+
schemaInput.addTopLevel(name, Ref.parse(`__ALL_TYPES__${reference}`));
|
|
32
|
+
}
|
|
33
|
+
const inputData = new InputData();
|
|
34
|
+
inputData.addInput(schemaInput);
|
|
35
|
+
// Update renderer options with the prepared schema
|
|
36
|
+
const rendererOptions = {
|
|
37
|
+
nexusOptions: {
|
|
38
|
+
nexusSchema: schema,
|
|
39
|
+
firstFilenameSansExtensions: this.options.firstFilenameSansExtensions,
|
|
40
|
+
},
|
|
41
|
+
...this.options.rendererOptions,
|
|
42
|
+
};
|
|
43
|
+
// Run quicktype and return
|
|
44
|
+
const returnValue = {};
|
|
45
|
+
const results = await quicktypeMultiFile({
|
|
46
|
+
inputData,
|
|
47
|
+
lang: this.options.lang,
|
|
48
|
+
rendererOptions,
|
|
49
|
+
});
|
|
50
|
+
results.forEach((contents, fileName) => (returnValue[fileName] = contents.lines.join("\n")));
|
|
51
|
+
return returnValue;
|
|
52
|
+
}
|
|
53
|
+
prepareSchema() {
|
|
54
|
+
const schema = {
|
|
55
|
+
services: {},
|
|
56
|
+
sharedJsonSchema: {
|
|
57
|
+
types: this.options.schema.types ?? {},
|
|
58
|
+
},
|
|
59
|
+
topLevelJsonSchemaTypes: {},
|
|
60
|
+
topLevelJsonSchemaLocalRefs: {},
|
|
61
|
+
};
|
|
62
|
+
for (const [serviceName, service] of Object.entries(this.options.schema.services || {})) {
|
|
63
|
+
schema.services[serviceName] = {
|
|
64
|
+
description: service.description,
|
|
65
|
+
operations: {},
|
|
66
|
+
};
|
|
67
|
+
for (const [operationName, operation] of Object.entries(service.operations)) {
|
|
68
|
+
const schemaOp = {
|
|
69
|
+
description: operation.description,
|
|
70
|
+
};
|
|
71
|
+
schema.services[serviceName].operations[operationName] = schemaOp;
|
|
72
|
+
if (operation.input) {
|
|
73
|
+
schemaOp.input = this.prepareInOutType(schema, serviceName, operationName, operation.input, "Input");
|
|
74
|
+
}
|
|
75
|
+
if (operation.output) {
|
|
76
|
+
schemaOp.output = this.prepareInOutType(schema, serviceName, operationName, operation.output, "Output");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return schema;
|
|
81
|
+
}
|
|
82
|
+
prepareInOutType(schema, serviceName, operationName, opInOut, suffix) {
|
|
83
|
+
// Check for an existing ref for this specific lang first
|
|
84
|
+
for (const langName of this.options.lang.names) {
|
|
85
|
+
if (Object.hasOwn(opInOut, `$${langName.toLowerCase()}Ref`)) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "existing",
|
|
88
|
+
name: opInOut[`$${langName.toLowerCase()}Ref`],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// If it's a single ref of a local "#" type, just set as reference
|
|
93
|
+
if (Object.hasOwn(opInOut, "$ref") && opInOut["$ref"].startsWith("#")) {
|
|
94
|
+
const reference = Ref.parse(opInOut["$ref"]);
|
|
95
|
+
const other = reference.lookupRef(schema.sharedJsonSchema);
|
|
96
|
+
if (other) {
|
|
97
|
+
// Default to the title, otherwise last element in path if it's a string key
|
|
98
|
+
let name = other.title;
|
|
99
|
+
if (!name) {
|
|
100
|
+
const lastReferenceElement = reference.path.at(-1);
|
|
101
|
+
if (lastReferenceElement?.kind == PathElementKind.KeyOrIndex) {
|
|
102
|
+
name = lastReferenceElement.key;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Only ref it if there is a name
|
|
106
|
+
if (name) {
|
|
107
|
+
// TODO(cretz): Check that this doesn't clash with something already there
|
|
108
|
+
schema.topLevelJsonSchemaLocalRefs[name] = opInOut["$ref"];
|
|
109
|
+
return { kind: "jsonSchema", name };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// TODO(cretz): Customization of generated names
|
|
114
|
+
// TODO(cretz): Remove the service name prefix by default
|
|
115
|
+
const name = `${serviceName}${operationName[0].toUpperCase()}${operationName.slice(1)}${suffix}`;
|
|
116
|
+
if (Object.hasOwn(schema.topLevelJsonSchemaTypes, name)) {
|
|
117
|
+
throw new Error(`Input/output for ${serviceName}.${operationName} would be named ${name} which clashes`);
|
|
118
|
+
}
|
|
119
|
+
schema.topLevelJsonSchemaTypes[name] = opInOut;
|
|
120
|
+
return { kind: "jsonSchema", name };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
class FetchDisabledSchemaStore extends JSONSchemaStore {
|
|
124
|
+
fetch(_address) {
|
|
125
|
+
// TODO(cretz): Support this?
|
|
126
|
+
throw new Error("External $ref unsupported");
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./definition-schema.js";
|
|
2
|
+
export { Generator, type GeneratorOptions } from "./generator.js";
|
|
3
|
+
export { CSharpLanguageWithNexus } from "./language-csharp.js";
|
|
4
|
+
export { GoLanguageWithNexus } from "./language-go.js";
|
|
5
|
+
export { JavaLanguageWithNexus } from "./language-java.js";
|
|
6
|
+
export { PythonLanguageWithNexus } from "./language-python.js";
|
|
7
|
+
export { TypeScriptLanguageWithNexus } from "./language-typescript.js";
|
|
8
|
+
export { parseFiles } from "./parser.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./definition-schema.js";
|
|
2
|
+
export { Generator } from "./generator.js";
|
|
3
|
+
export { CSharpLanguageWithNexus } from "./language-csharp.js";
|
|
4
|
+
export { GoLanguageWithNexus } from "./language-go.js";
|
|
5
|
+
export { JavaLanguageWithNexus } from "./language-java.js";
|
|
6
|
+
export { PythonLanguageWithNexus } from "./language-python.js";
|
|
7
|
+
export { TypeScriptLanguageWithNexus } from "./language-typescript.js";
|
|
8
|
+
export { parseFiles } from "./parser.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ConvenienceRenderer, CSharpTargetLanguage, type LanguageName, type RenderContext, type RendererOptions } from "quicktype-core";
|
|
2
|
+
export declare class CSharpLanguageWithNexus extends CSharpTargetLanguage {
|
|
3
|
+
protected makeRenderer<Lang extends LanguageName = "csharp">(renderContext: RenderContext, untypedOptionValues: RendererOptions<Lang>): ConvenienceRenderer;
|
|
4
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ConvenienceRenderer, cSharpOptions, CSharpRenderer, CSharpTargetLanguage, systemTextJsonCSharpOptions, SystemTextJsonCSharpRenderer, Type, } from "quicktype-core";
|
|
2
|
+
import { splitDescription } from "./utility.js";
|
|
3
|
+
import {} from "./generator.js";
|
|
4
|
+
import { namingFunction } from "quicktype-core/dist/language/CSharp/utils.js";
|
|
5
|
+
import { utf16StringEscape } from "quicktype-core/dist/support/Strings.js";
|
|
6
|
+
import { RenderAdapter } from "./render-adapter.js";
|
|
7
|
+
// Change some defaults globally
|
|
8
|
+
cSharpOptions.features.definition.defaultValue = "attributes-only";
|
|
9
|
+
cSharpOptions.framework.definition.defaultValue = "SystemTextJson";
|
|
10
|
+
cSharpOptions.namespace.definition.defaultValue = "NexusServices";
|
|
11
|
+
export class CSharpLanguageWithNexus extends CSharpTargetLanguage {
|
|
12
|
+
makeRenderer(renderContext, untypedOptionValues) {
|
|
13
|
+
const adapter = new CSharpRenderAdapter(super.makeRenderer(renderContext, untypedOptionValues), untypedOptionValues);
|
|
14
|
+
return adapter.makeRenderer({
|
|
15
|
+
emitSourceStructure(original, givenOutputFilename) {
|
|
16
|
+
original(givenOutputFilename);
|
|
17
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
18
|
+
},
|
|
19
|
+
emitTypesAndSupport(original) {
|
|
20
|
+
adapter.emitServices();
|
|
21
|
+
original();
|
|
22
|
+
},
|
|
23
|
+
emitUsings(original) {
|
|
24
|
+
adapter.emitUsings();
|
|
25
|
+
original();
|
|
26
|
+
},
|
|
27
|
+
emitDefaultLeadingComments(original) {
|
|
28
|
+
adapter.emitDefaultLeadingComments();
|
|
29
|
+
original();
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class CSharpRenderAdapter extends RenderAdapter {
|
|
35
|
+
makeFileName() {
|
|
36
|
+
// If there is a single service, use that, otherwise use the
|
|
37
|
+
// filename sans extensions to build it
|
|
38
|
+
const services = Object.entries(this.schema.services);
|
|
39
|
+
if (services.length == 1) {
|
|
40
|
+
return "I" + namingFunction.nameStyle(services[0][0]) + ".cs";
|
|
41
|
+
}
|
|
42
|
+
return (namingFunction.nameStyle(this.nexusRendererOptions.firstFilenameSansExtensions) + ".cs");
|
|
43
|
+
}
|
|
44
|
+
emitDefaultLeadingComments() {
|
|
45
|
+
// If it's System.Text.Json and not using helpers, we need to emit things
|
|
46
|
+
if (this.render instanceof SystemTextJsonCSharpRenderer &&
|
|
47
|
+
!this.render._csOptions.features.helpers) {
|
|
48
|
+
this.render.emitLine("// <auto-generated />");
|
|
49
|
+
this.render.emitLine("#pragma warning disable CS8618");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
emitUsings() {
|
|
53
|
+
this.render.emitLine("using NexusRpc;");
|
|
54
|
+
}
|
|
55
|
+
emitServices() {
|
|
56
|
+
for (const [serviceName, serviceSchema] of Object.entries(this.schema.services)) {
|
|
57
|
+
this.emitService(serviceName, serviceSchema);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
emitService(serviceName, serviceSchema) {
|
|
61
|
+
// TODO(cretz): Support force-set type name here
|
|
62
|
+
// TODO(cretz): Support a gen option to not add the "I"
|
|
63
|
+
// TODO(cretz): Put these I-prefixed names in the forbidden-top-level-names list
|
|
64
|
+
this.render.ensureBlankLine();
|
|
65
|
+
// Doc
|
|
66
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
67
|
+
const typeName = this.makeServiceTypeName("I" + namingFunction.nameStyle(serviceName));
|
|
68
|
+
this.render.emitLine("[NexusService",
|
|
69
|
+
// If the sans-I form doesn't match, set the name explicitly
|
|
70
|
+
serviceName == typeName.substring(1)
|
|
71
|
+
? []
|
|
72
|
+
: ['("', utf16StringEscape(serviceName), '")'], "]");
|
|
73
|
+
// Create interface itself
|
|
74
|
+
this.render.emitLine("public interface ", typeName);
|
|
75
|
+
const methodNamesInUse = {};
|
|
76
|
+
this.render.emitBlock(() => {
|
|
77
|
+
this.render.forEachWithBlankLines(Object.entries(serviceSchema.operations), "interposing", (op, opName, pos) => {
|
|
78
|
+
// TODO(cretz): Param and return tags
|
|
79
|
+
this.render.emitDescription(splitDescription(op.description));
|
|
80
|
+
// Convert the opName to C# style
|
|
81
|
+
// TODO(cretz): What about reserved words?
|
|
82
|
+
const methodName = this.makeOperationFunctionName(namingFunction.nameStyle(opName), methodNamesInUse);
|
|
83
|
+
// Create the attribute
|
|
84
|
+
this.render.emitLine("[NexusOperation",
|
|
85
|
+
// If the method name doesn't match op name, set explicitly
|
|
86
|
+
methodName == opName ? [] : ['("', utf16StringEscape(opName), '")'], "]");
|
|
87
|
+
const inType = this.getNexusType(op.input);
|
|
88
|
+
this.render.emitLine(this.getNexusType(op.output) ?? "void", " ", methodName, "(", inType ? [inType, " input"] : [], ");");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
getNexusType(reference) {
|
|
93
|
+
if (!reference) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
else if (reference.kind == "existing") {
|
|
97
|
+
return reference.name;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const type = this.render.topLevels.get(reference.name);
|
|
101
|
+
if (!type) {
|
|
102
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
103
|
+
}
|
|
104
|
+
return this.render.csType(type);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GoRenderer, GoTargetLanguage, type LanguageName, type RenderContext, type RendererOptions } from "quicktype-core";
|
|
2
|
+
import { BooleanOption } from "quicktype-core/dist/RendererOptions/index.js";
|
|
3
|
+
export declare const goWithNexusOptions: {
|
|
4
|
+
justTypes: BooleanOption<"just-types">;
|
|
5
|
+
justTypesAndPackage: BooleanOption<"just-types-and-package">;
|
|
6
|
+
packageName: import("quicktype-core/dist/RendererOptions/index.js").StringOption<"package">;
|
|
7
|
+
multiFileOutput: BooleanOption<"multi-file-output">;
|
|
8
|
+
fieldTags: import("quicktype-core/dist/RendererOptions/index.js").StringOption<"field-tags">;
|
|
9
|
+
omitEmpty: BooleanOption<"omit-empty">;
|
|
10
|
+
primitivePointers: BooleanOption<"primitive-pointers">;
|
|
11
|
+
};
|
|
12
|
+
export declare class GoLanguageWithNexus extends GoTargetLanguage {
|
|
13
|
+
getOptions(): typeof goWithNexusOptions;
|
|
14
|
+
protected makeRenderer<Lang extends LanguageName = "go">(renderContext: RenderContext, untypedOptionValues: RendererOptions<Lang>): GoRenderer;
|
|
15
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ConvenienceRenderer, goOptions, GoRenderer, GoTargetLanguage, Name, Namer, Type, } from "quicktype-core";
|
|
2
|
+
import { splitDescription } from "./utility.js";
|
|
3
|
+
import {} from "./generator.js";
|
|
4
|
+
import { RenderAdapter } from "./render-adapter.js";
|
|
5
|
+
import { stringEscape } from "quicktype-core/dist/support/Strings.js";
|
|
6
|
+
import { BooleanOption, getOptionValues, } from "quicktype-core/dist/RendererOptions/index.js";
|
|
7
|
+
import { primitiveValueTypeKinds } from "quicktype-core/dist/language/Golang/utils.js";
|
|
8
|
+
// Add options
|
|
9
|
+
export const goWithNexusOptions = {
|
|
10
|
+
primitivePointers: new BooleanOption("primitive-pointers", "Use pointers for nullable primitives", false),
|
|
11
|
+
...goOptions,
|
|
12
|
+
};
|
|
13
|
+
// Change some defaults globally
|
|
14
|
+
goWithNexusOptions.justTypesAndPackage.definition.defaultValue = true;
|
|
15
|
+
export class GoLanguageWithNexus extends GoTargetLanguage {
|
|
16
|
+
getOptions() {
|
|
17
|
+
return goWithNexusOptions;
|
|
18
|
+
}
|
|
19
|
+
makeRenderer(renderContext, untypedOptionValues) {
|
|
20
|
+
const adapter = new GoRenderAdapter(super.makeRenderer(renderContext, untypedOptionValues), untypedOptionValues);
|
|
21
|
+
adapter.assertValidOptions();
|
|
22
|
+
const options = getOptionValues(goWithNexusOptions, untypedOptionValues);
|
|
23
|
+
return adapter.makeRenderer({
|
|
24
|
+
emitSourceStructure(original) {
|
|
25
|
+
adapter.emitServices();
|
|
26
|
+
original();
|
|
27
|
+
adapter.render.finishFile(adapter.makeFileName());
|
|
28
|
+
},
|
|
29
|
+
emitTopLevel(original, t, name) {
|
|
30
|
+
// Do not emit __ALL_TYPES__ placeholder
|
|
31
|
+
if (name.firstProposedName(new Map()) == "__ALL_TYPES__") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
original(t, name);
|
|
35
|
+
},
|
|
36
|
+
nullableGoType(original, t, withIssues) {
|
|
37
|
+
// If the kind is a primitive and primitive pointers disabled, just return goType
|
|
38
|
+
if (!options.primitivePointers &&
|
|
39
|
+
primitiveValueTypeKinds.includes(t.kind)) {
|
|
40
|
+
return adapter.render.goType(t, withIssues);
|
|
41
|
+
}
|
|
42
|
+
return original(t, withIssues);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
class GoRenderAdapter extends RenderAdapter {
|
|
48
|
+
_imports;
|
|
49
|
+
assertValidOptions() {
|
|
50
|
+
// Currently, we only support single-file in Go
|
|
51
|
+
if (this.render._options.multiFileOutput) {
|
|
52
|
+
throw new Error("Multi-file output for Go not supported at this time");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
makeFileName() {
|
|
56
|
+
// If there is a single service, use that, otherwise use the
|
|
57
|
+
// filename sans extensions to build it
|
|
58
|
+
const services = Object.entries(this.schema.services);
|
|
59
|
+
const name = services.length == 1
|
|
60
|
+
? `${services[0][0]}.go`
|
|
61
|
+
: `${this.nexusRendererOptions.firstFilenameSansExtensions}.go`;
|
|
62
|
+
return name.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
// Key is qualified import, value is alias
|
|
65
|
+
get imports() {
|
|
66
|
+
if (this._imports === undefined) {
|
|
67
|
+
this._imports = {};
|
|
68
|
+
// Quicktype does not have a sophisticated import+alias construct for its
|
|
69
|
+
// renderer because they only ever needed one import. However, since we allow
|
|
70
|
+
// external types, we must support proper aliasing.
|
|
71
|
+
const origImports = this.render.collectAllImports();
|
|
72
|
+
if (!this.render._options.justTypes &&
|
|
73
|
+
!this.render._options.justTypesAndPackage) {
|
|
74
|
+
if (this.render.haveNamedUnions &&
|
|
75
|
+
!this.render._options.multiFileOutput) {
|
|
76
|
+
origImports.add("bytes");
|
|
77
|
+
origImports.add("errors");
|
|
78
|
+
}
|
|
79
|
+
origImports.add("encoding/json");
|
|
80
|
+
}
|
|
81
|
+
origImports.add("github.com/nexus-rpc/sdk-go/nexus");
|
|
82
|
+
for (const mport of origImports) {
|
|
83
|
+
this._imports[mport] = mport.split("/").pop();
|
|
84
|
+
}
|
|
85
|
+
// Add any external type reference pre-last-dots as imports
|
|
86
|
+
// TODO(cretz): Generics with qualified type args that need to be imported?
|
|
87
|
+
const externalTypes = Object.values(this.schema.services).flatMap((svc) => Object.values(svc.operations).flatMap((op) => [
|
|
88
|
+
op?.input?.kind == "existing" ? op.input.name : null,
|
|
89
|
+
op?.output?.kind == "existing" ? op.output.name : null,
|
|
90
|
+
]));
|
|
91
|
+
for (const externalType of externalTypes) {
|
|
92
|
+
const lastDot = externalType?.lastIndexOf(".") ?? -1;
|
|
93
|
+
if (externalType && lastDot > 0) {
|
|
94
|
+
const mport = externalType.slice(0, lastDot);
|
|
95
|
+
if (!Object.hasOwn(this._imports, mport)) {
|
|
96
|
+
// Append number until an unused alias is found
|
|
97
|
+
const origAlias = mport.split("/").pop();
|
|
98
|
+
let alias = origAlias;
|
|
99
|
+
let number_ = 0;
|
|
100
|
+
while (Object.values(this._imports).includes(alias)) {
|
|
101
|
+
alias = `${origAlias}${++number_}`;
|
|
102
|
+
}
|
|
103
|
+
this._imports[mport] = alias;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return this._imports;
|
|
109
|
+
}
|
|
110
|
+
emitServices() {
|
|
111
|
+
// Package decl
|
|
112
|
+
if (!this.render._options.justTypes ||
|
|
113
|
+
this.render._options.justTypesAndPackage) {
|
|
114
|
+
this.render.emitLineOnce("// Code generated by nexus-rpc-gen. DO NOT EDIT.");
|
|
115
|
+
this.render.ensureBlankLine();
|
|
116
|
+
const packageDeclaration = `package ${this.render._options.packageName}`;
|
|
117
|
+
this.render.emitLineOnce(packageDeclaration);
|
|
118
|
+
this.render.ensureBlankLine();
|
|
119
|
+
}
|
|
120
|
+
// Emit imports. To match goimports, we do all non-dot sorted, followed by blank line,
|
|
121
|
+
// then all dotted, sorted
|
|
122
|
+
const imports = Object.entries(this.imports).toSorted(([mportA, _a], [mportB, _b]) => mportA.localeCompare(mportB));
|
|
123
|
+
for (const [mport, alias] of imports.filter(([mport, _]) => !mport.includes("."))) {
|
|
124
|
+
const aliasPiece = alias != mport.split("/").pop() ? `${alias} ` : "";
|
|
125
|
+
// Must be a single string so it caches the full line for "once" so the
|
|
126
|
+
// Quicktype renderer doesn't render its own forms
|
|
127
|
+
this.render.emitLineOnce(`import ${aliasPiece}"${mport}"`);
|
|
128
|
+
}
|
|
129
|
+
this.render.ensureBlankLine();
|
|
130
|
+
for (const [mport, alias] of imports.filter(([mport, _]) => mport.includes("."))) {
|
|
131
|
+
const aliasPiece = alias != mport.split("/").pop() ? `${alias} ` : "";
|
|
132
|
+
this.render.emitLineOnce(`import ${aliasPiece}"${mport}"`);
|
|
133
|
+
}
|
|
134
|
+
// Emit each service
|
|
135
|
+
for (const [serviceName, serviceSchema] of Object.entries(this.schema.services)) {
|
|
136
|
+
this.emitService(serviceName, serviceSchema);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
emitService(serviceName, serviceSchema) {
|
|
140
|
+
this.render.ensureBlankLine();
|
|
141
|
+
const variableName = this.makeServiceTypeName(this.render.makeNamedTypeNamer().nameStyle(serviceName));
|
|
142
|
+
// Collect operations
|
|
143
|
+
const fieldNamesInUse = {};
|
|
144
|
+
const operations = Object.entries(serviceSchema.operations).flatMap(([opName, opSchema]) => ({
|
|
145
|
+
descPieces: splitDescription(opSchema.description) ?? [],
|
|
146
|
+
opName,
|
|
147
|
+
fieldName: this.makeOperationFunctionName(this.render.namerForObjectProperty().nameStyle(opName), fieldNamesInUse),
|
|
148
|
+
inType: this.getNexusType(opSchema.input) ?? "nexus.NoValue",
|
|
149
|
+
outType: this.getNexusType(opSchema.output) ?? "nexus.NoValue",
|
|
150
|
+
}));
|
|
151
|
+
// Create var with anonymous struct
|
|
152
|
+
this.render.emitDescription(splitDescription(serviceSchema.description));
|
|
153
|
+
this.render.emitLine("var ", variableName, " = struct {");
|
|
154
|
+
this.render.indent(() => {
|
|
155
|
+
this.render.emitTable([
|
|
156
|
+
[["ServiceName", " "], ["string"]],
|
|
157
|
+
...operations.flatMap((op) => {
|
|
158
|
+
const pieces = [];
|
|
159
|
+
if (op.descPieces.length > 0) {
|
|
160
|
+
pieces.push([op.descPieces.map((d) => `// ${d}`)]);
|
|
161
|
+
}
|
|
162
|
+
pieces.push([
|
|
163
|
+
[op.fieldName, " "],
|
|
164
|
+
["nexus.OperationReference[", op.inType, ", ", op.outType, "]"],
|
|
165
|
+
]);
|
|
166
|
+
return pieces;
|
|
167
|
+
}),
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
this.render.emitLine("}{");
|
|
171
|
+
this.render.indent(() => {
|
|
172
|
+
this.render.emitTable([
|
|
173
|
+
[["ServiceName:", " "], [`"${stringEscape(serviceName)}",`]],
|
|
174
|
+
...operations.map((op) => [
|
|
175
|
+
[op.fieldName + ":", " "],
|
|
176
|
+
[
|
|
177
|
+
"nexus.NewOperationReference[",
|
|
178
|
+
op.inType,
|
|
179
|
+
", ",
|
|
180
|
+
op.outType,
|
|
181
|
+
`]("${stringEscape(op.opName)}"),`,
|
|
182
|
+
],
|
|
183
|
+
]),
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
this.render.emitLine("}");
|
|
187
|
+
}
|
|
188
|
+
getNexusType(reference) {
|
|
189
|
+
if (!reference) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
else if (reference.kind == "existing") {
|
|
193
|
+
// If there is a dot, need to take qualified package and get alias
|
|
194
|
+
const lastDot = reference.name.lastIndexOf(".");
|
|
195
|
+
if (lastDot > 0) {
|
|
196
|
+
const mport = reference.name.slice(0, lastDot);
|
|
197
|
+
return `${this.imports[mport]}.${reference.name.slice(lastDot + 1)}`;
|
|
198
|
+
}
|
|
199
|
+
return reference.name;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const type = this.render.topLevels.get(reference.name);
|
|
203
|
+
if (!type) {
|
|
204
|
+
throw new Error(`Unable to find type for ${reference.name}`);
|
|
205
|
+
}
|
|
206
|
+
// If the type is primitive, use the alias
|
|
207
|
+
if (type.isPrimitive()) {
|
|
208
|
+
return reference.name;
|
|
209
|
+
}
|
|
210
|
+
return this.render.goType(type);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { JavaRenderer, JavaTargetLanguage, type LanguageName, type RenderContext, type RendererOptions } from "quicktype-core";
|
|
2
|
+
export declare class JavaLanguageWithNexus extends JavaTargetLanguage {
|
|
3
|
+
protected get defaultIndentation(): string;
|
|
4
|
+
protected makeRenderer<Lang extends LanguageName = "java">(renderContext: RenderContext, untypedOptionValues: RendererOptions<Lang>): JavaRenderer;
|
|
5
|
+
}
|