Skip to content

Commit 991016d

Browse files
committed
json schema small fix
1 parent 6c64bf5 commit 991016d

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

packages/core/src/backend/schema/jsonschema.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,36 @@ describe("JsonSchema generation", () => {
220220
expect(props["input1"]?.type).toBe("array");
221221
expect(props["input1"]?.items).toEqual({ type: "string" });
222222
});
223+
it("handles subcommand with flag wrapping", () => {
224+
const schema = schemaFor(
225+
minimalDescriptor({
226+
"command-line": "test [MASKS]",
227+
inputs: [
228+
{
229+
id: "masks",
230+
name: "Masks",
231+
"value-key": "[MASKS]",
232+
"command-line-flag": "--masks",
233+
type: {
234+
id: "masks",
235+
"command-line": "[FIXED] [MOVING]",
236+
inputs: [
237+
{ id: "fixed", "value-key": "[FIXED]", type: "String", optional: true },
238+
{ id: "moving", "value-key": "[MOVING]", type: "String", optional: false },
239+
],
240+
},
241+
optional: true,
242+
},
243+
],
244+
}),
245+
);
246+
const props = schema.properties as Record<string, JsonSchema>;
247+
const masks = props["masks"];
248+
expect(masks).toBeDefined();
249+
const masksProps = masks?.properties as Record<string, JsonSchema>;
250+
expect(masksProps["fixed"]?.type).toBe("string");
251+
expect(masksProps["moving"]?.type).toBe("string");
252+
});
223253
});
224254

225255
describe("JsonSchemaBackend", () => {

packages/core/src/backend/schema/jsonschema.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ class SchemaBuilder {
8282
}
8383
}
8484

85+
/** Find the sequence node whose child bindings match the struct's field names. */
86+
private findStructNode(
87+
node: Expr,
88+
type: Extract<BoundType, { kind: "struct" }>,
89+
): Extract<Expr, { kind: "sequence" }> | undefined {
90+
if (node.kind !== "sequence") return undefined;
91+
// Check if any child binding matches a field name
92+
for (const child of node.attrs.nodes) {
93+
const binding = this.ctx.resolve(child);
94+
if (binding && binding.name in type.fields) return node;
95+
}
96+
// Recurse into child sequences (handles collapsed flag sequences)
97+
for (const child of node.attrs.nodes) {
98+
const result = this.findStructNode(child, type);
99+
if (result) return result;
100+
}
101+
return undefined;
102+
}
103+
85104
private findTerminal(node: Expr): Expr {
86105
switch (node.kind) {
87106
case "optional":
@@ -122,8 +141,9 @@ class SchemaBuilder {
122141
const properties: Record<string, JsonSchema> = {};
123142
const required: string[] = [];
124143

125-
if (node?.kind === "sequence") {
126-
for (const child of node.attrs.nodes) {
144+
const structNode = node ? this.findStructNode(node, type) : undefined;
145+
if (structNode) {
146+
for (const child of structNode.attrs.nodes) {
127147
const childBinding = this.ctx.resolve(child);
128148
if (childBinding && childBinding.name in type.fields) {
129149
properties[childBinding.name] = this.fromBinding(childBinding);

0 commit comments

Comments
 (0)