@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.
@@ -0,0 +1,30 @@
1
+ import wordwrap from "wordwrap";
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 function proxyWithOverrides(instance, overrides) {
7
+ return new Proxy(instance, {
8
+ get(target, property, receiver) {
9
+ // @ts-expect-error index signature is fine for Proxy
10
+ const override = overrides[property];
11
+ if (typeof override === "function") {
12
+ const original = Reflect.get(target, property, receiver);
13
+ return function (...arguments_) {
14
+ // Call the override, passing original and args
15
+ return override.call(this, original.bind(this), ...arguments_);
16
+ };
17
+ }
18
+ return Reflect.get(target, property, receiver);
19
+ },
20
+ });
21
+ }
22
+ const wordWrap = wordwrap(90);
23
+ export function splitDescription(string_) {
24
+ if (!string_) {
25
+ return undefined;
26
+ }
27
+ return wordWrap(string_)
28
+ .split("\n")
29
+ .map((l) => l.trim());
30
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@nexus-rpc/gen-core",
3
+ "version": "0.1.0-alpha0",
4
+ "description": "Nexus code generation (core library)",
5
+ "author": "Temporal Technologies Inc. <sdk@temporal.io>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "bugs": {
11
+ "url": "https://github.com/nexus-rpc/nexus-rpc-gen/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/nexus-rpc/nexus-rpc-gen.git",
16
+ "directory": "src/packages/nexus-rpc-gen-core"
17
+ },
18
+ "homepage": "https://github.com/nexus-rpc/nexus-rpc-gen/tree/main/src/packages/nexus-rpc-gen-core",
19
+ "dependencies": {
20
+ "ajv": "^8.17.1",
21
+ "ajv-formats": "^3.0.1",
22
+ "quicktype-core": "^23.2.6",
23
+ "wordwrap": "^1.0.0",
24
+ "yaml": "^2.8.1"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "dist"
29
+ ],
30
+ "scripts": {}
31
+ }
@@ -0,0 +1,22 @@
1
+ /* eslint-disable */
2
+ // ⚠️ This file is generated. Do not edit manually.
3
+
4
+ /**
5
+ * Definition for Nexus RPC services and operations
6
+ */
7
+ export interface DefinitionSchema {
8
+ nexusrpc: string;
9
+ services?: { [key: string]: ServiceValue };
10
+ types?: { [key: string]: { [key: string]: any } };
11
+ }
12
+
13
+ export interface ServiceValue {
14
+ description?: string;
15
+ operations: { [key: string]: OperationValue };
16
+ }
17
+
18
+ export interface OperationValue {
19
+ description?: string;
20
+ input?: { [key: string]: any };
21
+ output?: { [key: string]: any };
22
+ }
@@ -0,0 +1,222 @@
1
+ import {
2
+ InputData,
3
+ JSONSchemaInput,
4
+ JSONSchemaStore,
5
+ quicktypeMultiFile,
6
+ Ref,
7
+ type JSONSchema,
8
+ type LanguageName,
9
+ type RendererOptions,
10
+ type TargetLanguage,
11
+ } from "quicktype-core";
12
+ import type { DefinitionSchema } from "./definition-schema";
13
+ import { PathElementKind } from "quicktype-core/dist/input/PathElement.js";
14
+ import { isPrimitiveTypeKind } from "quicktype-core/dist/Type/index.js";
15
+
16
+ export interface GeneratorOptions<Lang extends LanguageName = LanguageName> {
17
+ lang: TargetLanguage;
18
+ schema: DefinitionSchema;
19
+ rendererOptions: RendererOptions<Lang>;
20
+ // Used to help with ideal output filename
21
+ firstFilenameSansExtensions: string;
22
+ }
23
+
24
+ export interface PreparedSchema {
25
+ services: { [key: string]: PreparedService };
26
+ sharedJsonSchema: { types: { [key: string]: any } };
27
+ topLevelJsonSchemaTypes: { [key: string]: any };
28
+ topLevelJsonSchemaLocalRefs: { [key: string]: any };
29
+ }
30
+
31
+ export interface PreparedService {
32
+ description?: string;
33
+ operations: { [key: string]: PreparedOperation };
34
+ }
35
+
36
+ export interface PreparedOperation {
37
+ description?: string;
38
+ input?: PreparedTypeReference;
39
+ output?: PreparedTypeReference;
40
+ }
41
+
42
+ export interface PreparedTypeReference {
43
+ kind: "jsonSchema" | "existing";
44
+ name: string;
45
+ }
46
+
47
+ export interface NexusRendererOptions {
48
+ nexusSchema: PreparedSchema;
49
+ firstFilenameSansExtensions: string;
50
+ }
51
+
52
+ export function getNexusRendererOptions(
53
+ rendererOptions: RendererOptions,
54
+ ): NexusRendererOptions {
55
+ return (rendererOptions as any).nexusOptions;
56
+ }
57
+
58
+ export class Generator {
59
+ private readonly options: GeneratorOptions;
60
+
61
+ constructor(options: GeneratorOptions) {
62
+ this.options = options;
63
+ }
64
+
65
+ async generate(): Promise<{ [fileName: string]: string }> {
66
+ // Prepare schema
67
+ const schema = this.prepareSchema();
68
+
69
+ // Build quicktype input
70
+ const schemaInput = new JSONSchemaInput(new FetchDisabledSchemaStore());
71
+ const jsonSchema = {
72
+ ...schema.sharedJsonSchema,
73
+ ...schema.topLevelJsonSchemaTypes,
74
+ };
75
+ await schemaInput.addSource({
76
+ // TODO(cretz): Give proper filename name here for proper cross-file referencing
77
+ name: "__ALL_TYPES__",
78
+ schema: JSON.stringify(jsonSchema),
79
+ });
80
+ // Set the top-level types
81
+ for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
82
+ schemaInput.addTopLevel(
83
+ topLevel,
84
+ Ref.parse(`__ALL_TYPES__#/${topLevel}`),
85
+ );
86
+ }
87
+ for (const [name, reference] of Object.entries(
88
+ schema.topLevelJsonSchemaLocalRefs,
89
+ )) {
90
+ schemaInput.addTopLevel(name, Ref.parse(`__ALL_TYPES__${reference}`));
91
+ }
92
+ const inputData = new InputData();
93
+ inputData.addInput(schemaInput);
94
+
95
+ // Update renderer options with the prepared schema
96
+ const rendererOptions = {
97
+ nexusOptions: {
98
+ nexusSchema: schema,
99
+ firstFilenameSansExtensions: this.options.firstFilenameSansExtensions,
100
+ },
101
+ ...this.options.rendererOptions,
102
+ };
103
+
104
+ // Run quicktype and return
105
+ const returnValue: { [fileName: string]: string } = {};
106
+ const results = await quicktypeMultiFile({
107
+ inputData,
108
+ lang: this.options.lang,
109
+ rendererOptions,
110
+ });
111
+ results.forEach(
112
+ (contents, fileName) =>
113
+ (returnValue[fileName] = contents.lines.join("\n")),
114
+ );
115
+ return returnValue;
116
+ }
117
+
118
+ private prepareSchema(): PreparedSchema {
119
+ const schema: PreparedSchema = {
120
+ services: {},
121
+ sharedJsonSchema: {
122
+ types: this.options.schema.types ?? {},
123
+ },
124
+ topLevelJsonSchemaTypes: {},
125
+ topLevelJsonSchemaLocalRefs: {},
126
+ };
127
+ for (const [serviceName, service] of Object.entries(
128
+ this.options.schema.services || {},
129
+ )) {
130
+ schema.services[serviceName] = {
131
+ description: service.description,
132
+ operations: {},
133
+ };
134
+ for (const [operationName, operation] of Object.entries(
135
+ service.operations,
136
+ )) {
137
+ const schemaOp: PreparedOperation = {
138
+ description: operation.description,
139
+ };
140
+ schema.services[serviceName].operations[operationName] = schemaOp;
141
+ if (operation.input) {
142
+ schemaOp.input = this.prepareInOutType(
143
+ schema,
144
+ serviceName,
145
+ operationName,
146
+ operation.input,
147
+ "Input",
148
+ );
149
+ }
150
+ if (operation.output) {
151
+ schemaOp.output = this.prepareInOutType(
152
+ schema,
153
+ serviceName,
154
+ operationName,
155
+ operation.output,
156
+ "Output",
157
+ );
158
+ }
159
+ }
160
+ }
161
+ return schema;
162
+ }
163
+
164
+ private prepareInOutType(
165
+ schema: PreparedSchema,
166
+ serviceName: string,
167
+ operationName: string,
168
+ opInOut: any,
169
+ suffix: string,
170
+ ): PreparedTypeReference {
171
+ // Check for an existing ref for this specific lang first
172
+ for (const langName of this.options.lang.names) {
173
+ if (Object.hasOwn(opInOut, `$${langName.toLowerCase()}Ref`)) {
174
+ return {
175
+ kind: "existing",
176
+ name: opInOut[`$${langName.toLowerCase()}Ref`],
177
+ };
178
+ }
179
+ }
180
+
181
+ // If it's a single ref of a local "#" type, just set as reference
182
+ if (Object.hasOwn(opInOut, "$ref") && opInOut["$ref"].startsWith("#")) {
183
+ const reference = Ref.parse(opInOut["$ref"]);
184
+ const other = reference.lookupRef(schema.sharedJsonSchema) as {
185
+ title?: string;
186
+ };
187
+ if (other) {
188
+ // Default to the title, otherwise last element in path if it's a string key
189
+ let name = other.title;
190
+ if (!name) {
191
+ const lastReferenceElement = reference.path.at(-1);
192
+ if (lastReferenceElement?.kind == PathElementKind.KeyOrIndex) {
193
+ name = lastReferenceElement.key;
194
+ }
195
+ }
196
+ // Only ref it if there is a name
197
+ if (name) {
198
+ // TODO(cretz): Check that this doesn't clash with something already there
199
+ schema.topLevelJsonSchemaLocalRefs[name] = opInOut["$ref"];
200
+ return { kind: "jsonSchema", name };
201
+ }
202
+ }
203
+ }
204
+ // TODO(cretz): Customization of generated names
205
+ // TODO(cretz): Remove the service name prefix by default
206
+ const name = `${serviceName}${operationName[0].toUpperCase()}${operationName.slice(1)}${suffix}`;
207
+ if (Object.hasOwn(schema.topLevelJsonSchemaTypes, name)) {
208
+ throw new Error(
209
+ `Input/output for ${serviceName}.${operationName} would be named ${name} which clashes`,
210
+ );
211
+ }
212
+ schema.topLevelJsonSchemaTypes[name] = opInOut;
213
+ return { kind: "jsonSchema", name };
214
+ }
215
+ }
216
+
217
+ class FetchDisabledSchemaStore extends JSONSchemaStore {
218
+ fetch(_address: string): Promise<JSONSchema | undefined> {
219
+ // TODO(cretz): Support this?
220
+ throw new Error("External $ref unsupported");
221
+ }
222
+ }
package/src/index.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";
@@ -0,0 +1,190 @@
1
+ import {
2
+ ConvenienceRenderer,
3
+ cSharpOptions,
4
+ CSharpRenderer,
5
+ CSharpTargetLanguage,
6
+ systemTextJsonCSharpOptions,
7
+ SystemTextJsonCSharpRenderer,
8
+ Type,
9
+ type LanguageName,
10
+ type OptionValues,
11
+ type RenderContext,
12
+ type RendererOptions,
13
+ type Sourcelike,
14
+ } from "quicktype-core";
15
+ import { splitDescription } from "./utility.js";
16
+ import {
17
+ type PreparedService,
18
+ type PreparedTypeReference,
19
+ } from "./generator.js";
20
+ import { namingFunction } from "quicktype-core/dist/language/CSharp/utils.js";
21
+ import { utf16StringEscape } from "quicktype-core/dist/support/Strings.js";
22
+ import { RenderAdapter, type RenderAccessible } from "./render-adapter.js";
23
+
24
+ // Change some defaults globally
25
+ cSharpOptions.features.definition.defaultValue = "attributes-only";
26
+ cSharpOptions.framework.definition.defaultValue = "SystemTextJson";
27
+ cSharpOptions.namespace.definition.defaultValue = "NexusServices";
28
+
29
+ export class CSharpLanguageWithNexus extends CSharpTargetLanguage {
30
+ protected override makeRenderer<Lang extends LanguageName = "csharp">(
31
+ renderContext: RenderContext,
32
+ untypedOptionValues: RendererOptions<Lang>,
33
+ ): ConvenienceRenderer {
34
+ const adapter = new CSharpRenderAdapter(
35
+ super.makeRenderer(renderContext, untypedOptionValues),
36
+ untypedOptionValues,
37
+ );
38
+
39
+ return adapter.makeRenderer({
40
+ emitSourceStructure(original, givenOutputFilename) {
41
+ original(givenOutputFilename);
42
+ adapter.render.finishFile(adapter.makeFileName());
43
+ },
44
+ emitTypesAndSupport(original) {
45
+ adapter.emitServices();
46
+ original();
47
+ },
48
+ emitUsings(original) {
49
+ adapter.emitUsings();
50
+ original();
51
+ },
52
+ emitDefaultLeadingComments(original) {
53
+ adapter.emitDefaultLeadingComments();
54
+ original();
55
+ },
56
+ });
57
+ }
58
+ }
59
+
60
+ // emitTypesAndSupport is private, omit before adding
61
+ type CSharpRenderAccessible = Omit<CSharpRenderer, "emitTypesAndSupport"> &
62
+ RenderAccessible & {
63
+ readonly _csOptions: OptionValues<typeof cSharpOptions>;
64
+ csType(
65
+ t: Type,
66
+ follow?: (t: Type) => Type,
67
+ withIssues?: boolean,
68
+ ): Sourcelike;
69
+ emitBlock(f: () => void): void;
70
+ emitDefaultLeadingComments(): void;
71
+ emitTypesAndSupport(): void;
72
+ emitUsings(): void;
73
+ };
74
+
75
+ class CSharpRenderAdapter extends RenderAdapter<CSharpRenderAccessible> {
76
+ makeFileName() {
77
+ // If there is a single service, use that, otherwise use the
78
+ // filename sans extensions to build it
79
+ const services = Object.entries(this.schema.services);
80
+ if (services.length == 1) {
81
+ return "I" + namingFunction.nameStyle(services[0][0]) + ".cs";
82
+ }
83
+ return (
84
+ namingFunction.nameStyle(
85
+ this.nexusRendererOptions.firstFilenameSansExtensions,
86
+ ) + ".cs"
87
+ );
88
+ }
89
+
90
+ emitDefaultLeadingComments() {
91
+ // If it's System.Text.Json and not using helpers, we need to emit things
92
+ if (
93
+ (this.render as any) instanceof SystemTextJsonCSharpRenderer &&
94
+ !(
95
+ this.render._csOptions as OptionValues<
96
+ typeof systemTextJsonCSharpOptions
97
+ >
98
+ ).features.helpers
99
+ ) {
100
+ this.render.emitLine("// <auto-generated />");
101
+ this.render.emitLine("#pragma warning disable CS8618");
102
+ }
103
+ }
104
+
105
+ emitUsings() {
106
+ this.render.emitLine("using NexusRpc;");
107
+ }
108
+
109
+ emitServices() {
110
+ for (const [serviceName, serviceSchema] of Object.entries(
111
+ this.schema.services,
112
+ )) {
113
+ this.emitService(serviceName, serviceSchema);
114
+ }
115
+ }
116
+
117
+ emitService(serviceName: string, serviceSchema: PreparedService) {
118
+ // TODO(cretz): Support force-set type name here
119
+ // TODO(cretz): Support a gen option to not add the "I"
120
+ // TODO(cretz): Put these I-prefixed names in the forbidden-top-level-names list
121
+ this.render.ensureBlankLine();
122
+
123
+ // Doc
124
+ this.render.emitDescription(splitDescription(serviceSchema.description));
125
+
126
+ const typeName = this.makeServiceTypeName(
127
+ "I" + namingFunction.nameStyle(serviceName),
128
+ );
129
+ this.render.emitLine(
130
+ "[NexusService",
131
+ // If the sans-I form doesn't match, set the name explicitly
132
+ serviceName == typeName.substring(1)
133
+ ? []
134
+ : ['("', utf16StringEscape(serviceName), '")'],
135
+ "]",
136
+ );
137
+
138
+ // Create interface itself
139
+ this.render.emitLine("public interface ", typeName);
140
+ const methodNamesInUse = {};
141
+ this.render.emitBlock(() => {
142
+ this.render.forEachWithBlankLines(
143
+ Object.entries(serviceSchema.operations),
144
+ "interposing",
145
+ (op, opName, pos) => {
146
+ // TODO(cretz): Param and return tags
147
+ this.render.emitDescription(splitDescription(op.description));
148
+ // Convert the opName to C# style
149
+ // TODO(cretz): What about reserved words?
150
+ const methodName = this.makeOperationFunctionName(
151
+ namingFunction.nameStyle(opName),
152
+ methodNamesInUse,
153
+ );
154
+ // Create the attribute
155
+ this.render.emitLine(
156
+ "[NexusOperation",
157
+ // If the method name doesn't match op name, set explicitly
158
+ methodName == opName ? [] : ['("', utf16StringEscape(opName), '")'],
159
+ "]",
160
+ );
161
+ const inType = this.getNexusType(op.input);
162
+ this.render.emitLine(
163
+ this.getNexusType(op.output) ?? "void",
164
+ " ",
165
+ methodName,
166
+ "(",
167
+ inType ? [inType, " input"] : [],
168
+ ");",
169
+ );
170
+ },
171
+ );
172
+ });
173
+ }
174
+
175
+ getNexusType(
176
+ reference: PreparedTypeReference | undefined,
177
+ ): Sourcelike | undefined {
178
+ if (!reference) {
179
+ return undefined;
180
+ } else if (reference.kind == "existing") {
181
+ return reference.name;
182
+ } else {
183
+ const type = this.render.topLevels.get(reference.name);
184
+ if (!type) {
185
+ throw new Error(`Unable to find type for ${reference.name}`);
186
+ }
187
+ return this.render.csType(type);
188
+ }
189
+ }
190
+ }