-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add $ref support to all four language code generators #1062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,11 +10,14 @@ import { execFile } from "child_process"; | |
| import fs from "fs/promises"; | ||
| import path from "path"; | ||
| import { promisify } from "util"; | ||
| import type { JSONSchema7 } from "json-schema"; | ||
| import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; | ||
| import { | ||
| getSessionEventsSchemaPath, | ||
| getApiSchemaPath, | ||
| writeGeneratedFile, | ||
| collectDefinitions, | ||
| resolveRef, | ||
| refTypeName, | ||
| isRpcMethod, | ||
| isNodeFullyExperimental, | ||
| EXCLUDED_EVENT_TYPES, | ||
|
|
@@ -199,6 +202,9 @@ interface EventVariant { | |
|
|
||
| let generatedEnums = new Map<string, { enumName: string; values: string[] }>(); | ||
|
|
||
| /** Schema definitions available during session event generation (for $ref resolution). */ | ||
| let sessionDefinitions: Record<string, JSONSchema7Definition> = {}; | ||
|
|
||
| function getOrCreateEnum(parentClassName: string, propName: string, values: string[], enumOutput: string[], description?: string): string { | ||
| const valuesKey = [...values].sort().join("|"); | ||
| for (const [, existing] of generatedEnums) { | ||
|
|
@@ -402,6 +408,21 @@ function resolveSessionPropertyType( | |
| nestedClasses: Map<string, string>, | ||
| enumOutput: string[] | ||
| ): string { | ||
| // Handle $ref by resolving against schema definitions | ||
| if (propSchema.$ref) { | ||
| const typeName = refTypeName(propSchema.$ref); | ||
| const className = typeToClassName(typeName); | ||
| if (!nestedClasses.has(className)) { | ||
| const refSchema = resolveRef(propSchema.$ref, sessionDefinitions); | ||
| if (refSchema) { | ||
| if (refSchema.enum && Array.isArray(refSchema.enum)) { | ||
| return getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput); | ||
| } | ||
| nestedClasses.set(className, generateNestedClass(className, refSchema, knownTypes, nestedClasses, enumOutput)); | ||
| } | ||
| } | ||
| return isRequired ? className : `${className}?`; | ||
| } | ||
| if (propSchema.anyOf) { | ||
| const hasNull = propSchema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); | ||
| const nonNull = propSchema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); | ||
|
|
@@ -433,6 +454,18 @@ function resolveSessionPropertyType( | |
| } | ||
| if (propSchema.type === "array" && propSchema.items) { | ||
| const items = propSchema.items as JSONSchema7; | ||
| // Handle $ref in array items | ||
| if (items.$ref) { | ||
| const typeName = refTypeName(items.$ref); | ||
| const className = typeToClassName(typeName); | ||
| if (!nestedClasses.has(className)) { | ||
| const refSchema = resolveRef(items.$ref, sessionDefinitions); | ||
| if (refSchema) { | ||
| nestedClasses.set(className, generateNestedClass(className, refSchema, knownTypes, nestedClasses, enumOutput)); | ||
| } | ||
| } | ||
| return isRequired ? `${className}[]` : `${className}[]?`; | ||
| } | ||
| // Array of discriminated union (anyOf with shared discriminator) | ||
| if (items.anyOf && Array.isArray(items.anyOf)) { | ||
| const variants = items.anyOf.filter((v): v is JSONSchema7 => typeof v === "object"); | ||
|
|
@@ -491,6 +524,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map<string, string | |
|
|
||
| function generateSessionEventsCode(schema: JSONSchema7): string { | ||
| generatedEnums.clear(); | ||
| sessionDefinitions = schema.definitions as Record<string, JSONSchema7Definition> || {}; | ||
| const variants = extractEventVariants(schema); | ||
|
Comment on lines
525
to
528
|
||
| const knownTypes = new Map<string, string>(); | ||
| const nestedClasses = new Map<string, string>(); | ||
|
|
@@ -600,6 +634,9 @@ let experimentalRpcTypes = new Set<string>(); | |
| let rpcKnownTypes = new Map<string, string>(); | ||
| let rpcEnumOutput: string[] = []; | ||
|
|
||
| /** Schema definitions available during RPC generation (for $ref resolution). */ | ||
| let rpcDefinitions: Record<string, JSONSchema7Definition> = {}; | ||
|
|
||
| function singularPascal(s: string): string { | ||
| const p = toPascalCase(s); | ||
| if (p.endsWith("ies")) return `${p.slice(0, -3)}y`; | ||
|
|
@@ -617,6 +654,16 @@ function paramsTypeName(rpcMethod: string): string { | |
| } | ||
|
|
||
| function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassName: string, propName: string, classes: string[]): string { | ||
| // Handle $ref by resolving against schema definitions and generating the referenced class | ||
| if (schema.$ref) { | ||
| const typeName = refTypeName(schema.$ref); | ||
| const refSchema = resolveRef(schema.$ref, rpcDefinitions); | ||
| if (refSchema && !emittedRpcClasses.has(typeName)) { | ||
| const cls = emitRpcClass(typeName, refSchema, "public", classes); | ||
| if (cls) classes.push(cls); | ||
| } | ||
| return isRequired ? typeName : `${typeName}?`; | ||
| } | ||
| // Handle anyOf: [T, null] → T? (nullable typed property) | ||
| if (schema.anyOf) { | ||
| const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); | ||
|
|
@@ -637,6 +684,16 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam | |
| } | ||
| if (schema.type === "array" && schema.items) { | ||
| const items = schema.items as JSONSchema7; | ||
| // Handle $ref in array items | ||
| if (items.$ref) { | ||
| const typeName = refTypeName(items.$ref); | ||
| const refSchema = resolveRef(items.$ref, rpcDefinitions); | ||
| if (refSchema && !emittedRpcClasses.has(typeName)) { | ||
| const cls = emitRpcClass(typeName, refSchema, "public", classes); | ||
| if (cls) classes.push(cls); | ||
| } | ||
| return isRequired ? `List<${typeName}>` : `List<${typeName}>?`; | ||
| } | ||
| if (items.type === "object" && items.properties) { | ||
| const itemClass = singularPascal(propName); | ||
| if (!emittedRpcClasses.has(itemClass)) classes.push(emitRpcClass(itemClass, items, "public", classes)); | ||
|
|
@@ -1065,6 +1122,7 @@ function generateRpcCode(schema: ApiSchema): string { | |
| rpcKnownTypes.clear(); | ||
| rpcEnumOutput = []; | ||
| generatedEnums.clear(); // Clear shared enum deduplication map | ||
| rpcDefinitions = collectDefinitions(schema as Record<string, unknown>); | ||
| const classes: string[] = []; | ||
|
|
||
| let serverRpcParts: string[] = []; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
|
|
||
| import { execFile } from "child_process"; | ||
| import fs from "fs/promises"; | ||
| import type { JSONSchema7 } from "json-schema"; | ||
| import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; | ||
| import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from "quicktype-core"; | ||
| import { promisify } from "util"; | ||
| import { | ||
|
|
@@ -19,6 +19,9 @@ import { | |
| isRpcMethod, | ||
| postProcessSchema, | ||
| writeGeneratedFile, | ||
| collectDefinitions, | ||
| refTypeName, | ||
| resolveRef, | ||
| type ApiSchema, | ||
| type RpcMethod, | ||
| } from "./utils.js"; | ||
|
|
@@ -152,6 +155,7 @@ interface GoCodegenCtx { | |
| enums: string[]; | ||
| enumsByValues: Map<string, string>; // sorted-values-key → enumName | ||
| generatedNames: Set<string>; | ||
| definitions?: Record<string, JSONSchema7Definition>; | ||
| } | ||
|
|
||
| function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] { | ||
|
|
@@ -257,6 +261,21 @@ function resolveGoPropertyType( | |
| ): string { | ||
| const nestedName = parentTypeName + toGoFieldName(jsonPropName); | ||
|
|
||
| // Handle $ref — resolve the reference and generate the referenced type | ||
| if (propSchema.$ref && typeof propSchema.$ref === "string") { | ||
| const typeName = toGoFieldName(refTypeName(propSchema.$ref)); | ||
| const resolved = resolveRef(propSchema.$ref, ctx.definitions); | ||
| if (resolved) { | ||
| if (resolved.enum) { | ||
| return getOrCreateGoEnum(typeName, resolved.enum as string[], ctx, resolved.description); | ||
| } | ||
| emitGoStruct(typeName, resolved, ctx); | ||
| return isRequired ? typeName : `*${typeName}`; | ||
| } | ||
| // Fallback: use the type name directly | ||
| return isRequired ? typeName : `*${typeName}`; | ||
| } | ||
|
|
||
| // Handle anyOf | ||
| if (propSchema.anyOf) { | ||
| const nonNull = (propSchema.anyOf as JSONSchema7[]).filter((s) => s.type !== "null"); | ||
|
|
@@ -514,6 +533,7 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string { | |
| enums: [], | ||
| enumsByValues: new Map(), | ||
| generatedNames: new Set(), | ||
| definitions: schema.definitions as Record<string, JSONSchema7Definition> | undefined, | ||
| }; | ||
|
|
||
| // Generate per-event data structs | ||
|
|
@@ -802,10 +822,12 @@ async function generateRpc(schemaPath?: string): Promise<void> { | |
| ...collectRpcMethods(schema.clientSession || {}), | ||
| ]; | ||
|
|
||
| // Build a combined schema for quicktype - prefix types to avoid conflicts | ||
| // Build a combined schema for quicktype — prefix types to avoid conflicts. | ||
| // Include shared definitions from the API schema for $ref resolution. | ||
| const sharedDefs = collectDefinitions(schema as Record<string, unknown>); | ||
| const combinedSchema: JSONSchema7 = { | ||
| $schema: "http://json-schema.org/draft-07/schema#", | ||
| definitions: {}, | ||
| definitions: { ...sharedDefs }, | ||
| }; | ||
|
|
||
|
Comment on lines
+825
to
832
|
||
| for (const method of allMethods) { | ||
|
|
@@ -832,10 +854,14 @@ async function generateRpc(schemaPath?: string): Promise<void> { | |
| } | ||
| } | ||
|
|
||
| // Generate types via quicktype | ||
| // Generate types via quicktype — include all definitions in each source for $ref resolution | ||
| const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); | ||
| for (const [name, def] of Object.entries(combinedSchema.definitions!)) { | ||
| await schemaInput.addSource({ name, schema: JSON.stringify(def) }); | ||
| const schemaWithDefs: JSONSchema7 = { | ||
| ...(typeof def === "object" ? (def as JSONSchema7) : {}), | ||
| definitions: combinedSchema.definitions, | ||
| }; | ||
| await schemaInput.addSource({ name, schema: JSON.stringify(schemaWithDefs) }); | ||
| } | ||
|
|
||
| const inputData = new InputData(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |||||||||||||||
| isRpcMethod, | ||||||||||||||||
| postProcessSchema, | ||||||||||||||||
| writeGeneratedFile, | ||||||||||||||||
| collectDefinitions, | ||||||||||||||||
| isRpcMethod, | ||||||||||||||||
| isNodeFullyExperimental, | ||||||||||||||||
| type ApiSchema, | ||||||||||||||||
|
|
@@ -151,11 +152,20 @@ async function generateSessionEvents(schemaPath?: string): Promise<void> { | |||||||||||||||
|
|
||||||||||||||||
| const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); | ||||||||||||||||
| const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; | ||||||||||||||||
| const resolvedSchema = (schema.definitions?.SessionEvent as JSONSchema7) || schema; | ||||||||||||||||
| const processed = postProcessSchema(resolvedSchema); | ||||||||||||||||
| const processed = postProcessSchema(schema); | ||||||||||||||||
|
|
||||||||||||||||
| // Extract SessionEvent as root but keep all other definitions for $ref resolution | ||||||||||||||||
| const sessionEventDef = (processed.definitions?.SessionEvent as JSONSchema7) || processed; | ||||||||||||||||
| const otherDefs = Object.fromEntries( | ||||||||||||||||
| Object.entries(processed.definitions || {}).filter(([key]) => key !== "SessionEvent") | ||||||||||||||||
| ); | ||||||||||||||||
| const schemaForQuicktype: JSONSchema7 = { | ||||||||||||||||
| ...sessionEventDef, | ||||||||||||||||
| ...(Object.keys(otherDefs).length > 0 ? { definitions: otherDefs } : {}), | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); | ||||||||||||||||
| await schemaInput.addSource({ name: "SessionEvent", schema: JSON.stringify(processed) }); | ||||||||||||||||
| await schemaInput.addSource({ name: "SessionEvent", schema: JSON.stringify(schemaForQuicktype) }); | ||||||||||||||||
|
|
||||||||||||||||
| const inputData = new InputData(); | ||||||||||||||||
| inputData.addInput(schemaInput); | ||||||||||||||||
|
|
@@ -214,10 +224,11 @@ async function generateRpc(schemaPath?: string): Promise<void> { | |||||||||||||||
| ...collectRpcMethods(schema.clientSession || {}), | ||||||||||||||||
| ]; | ||||||||||||||||
|
|
||||||||||||||||
| // Build a combined schema for quicktype | ||||||||||||||||
| // Build a combined schema for quicktype, including shared definitions from the API schema | ||||||||||||||||
| const sharedDefs = collectDefinitions(schema as Record<string, unknown>); | ||||||||||||||||
| const combinedSchema: JSONSchema7 = { | ||||||||||||||||
| $schema: "http://json-schema.org/draft-07/schema#", | ||||||||||||||||
| definitions: {}, | ||||||||||||||||
| definitions: { ...sharedDefs }, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| for (const method of allMethods) { | ||||||||||||||||
|
|
@@ -243,10 +254,14 @@ async function generateRpc(schemaPath?: string): Promise<void> { | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Generate types via quicktype | ||||||||||||||||
| // Generate types via quicktype — include all definitions in each source for $ref resolution | ||||||||||||||||
| const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); | ||||||||||||||||
| for (const [name, def] of Object.entries(combinedSchema.definitions!)) { | ||||||||||||||||
| await schemaInput.addSource({ name, schema: JSON.stringify(def) }); | ||||||||||||||||
| const schemaWithDefs: JSONSchema7 = { | ||||||||||||||||
| ...(typeof def === "object" ? (def as JSONSchema7) : {}), | ||||||||||||||||
| definitions: combinedSchema.definitions, | ||||||||||||||||
|
Comment on lines
+260
to
+262
|
||||||||||||||||
| const schemaWithDefs: JSONSchema7 = { | |
| ...(typeof def === "object" ? (def as JSONSchema7) : {}), | |
| definitions: combinedSchema.definitions, | |
| const schemaWithDefs: JSONSchema7 & { $defs?: JSONSchema7["definitions"] } = { | |
| ...(typeof def === "object" ? (def as JSONSchema7) : {}), | |
| definitions: combinedSchema.definitions, | |
| $defs: combinedSchema.definitions, |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { | |||||||||||||||||||||
| getApiSchemaPath, | ||||||||||||||||||||||
| postProcessSchema, | ||||||||||||||||||||||
| writeGeneratedFile, | ||||||||||||||||||||||
| collectDefinitions, | ||||||||||||||||||||||
| isRpcMethod, | ||||||||||||||||||||||
| isNodeFullyExperimental, | ||||||||||||||||||||||
| type ApiSchema, | ||||||||||||||||||||||
|
|
@@ -88,32 +89,58 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; | |||||||||||||||||||||
| const allMethods = [...collectRpcMethods(schema.server || {}), ...collectRpcMethods(schema.session || {})]; | ||||||||||||||||||||||
| const clientSessionMethods = collectRpcMethods(schema.clientSession || {}); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Build a single combined schema with shared definitions and all method types. | ||||||||||||||||||||||
| // This ensures $ref-referenced types are generated exactly once. | ||||||||||||||||||||||
| const sharedDefs = collectDefinitions(schema as Record<string, unknown>); | ||||||||||||||||||||||
| const combinedSchema: JSONSchema7 = { | ||||||||||||||||||||||
| $schema: "http://json-schema.org/draft-07/schema#", | ||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||
| definitions: { ...sharedDefs }, | ||||||||||||||||||||||
|
Comment on lines
+95
to
+98
|
||||||||||||||||||||||
| const combinedSchema: JSONSchema7 = { | |
| $schema: "http://json-schema.org/draft-07/schema#", | |
| type: "object", | |
| definitions: { ...sharedDefs }, | |
| const sharedDefinitions: Record<string, unknown> = { ...sharedDefs }; | |
| const combinedSchema: JSONSchema7 & { $defs: Record<string, unknown> } = { | |
| $schema: "http://json-schema.org/draft-07/schema#", | |
| type: "object", | |
| definitions: sharedDefinitions, | |
| $defs: sharedDefinitions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For
$refto an enum schema, the code returns the enum type name directly, bypassing theisRequirednullability handling. This makes optional enum properties non-nullable. Apply?whenisRequiredis false (and keep enum generation/dedup logic intact).