Skip to content

Commit 72b9628

Browse files
authored
Merge pull request #19 from lucianfialho/fix/15-object-params-as-strings
fix: parse object and array params from JSON strings
2 parents 4dc2bfa + c670891 commit 72b9628

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

src/executor/commander-builder.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { Command } from "commander";
33
import { loadSpec } from "../parser/loader.js";
44
import { extractOperations } from "../parser/extractor.js";
55
import { buildCommands } from "./commander-builder.js";
66
import type { RuntimeConfig } from "./types.js";
7+
import type { Operation } from "../parser/types.js";
78
import path from "node:path";
89

910
const FIXTURE = path.resolve("test/fixtures/petstore.yaml");
@@ -15,6 +16,8 @@ const config: RuntimeConfig = {
1516
output: "json",
1617
verbose: false,
1718
quiet: false,
19+
dryRun: false,
20+
validate: false,
1821
};
1922

2023
describe("buildCommands", () => {
@@ -103,3 +106,121 @@ describe("buildCommands", () => {
103106
expect(listCmd.description()).toBe("List all pets");
104107
});
105108
});
109+
110+
describe("collectParams JSON parsing", () => {
111+
beforeEach(() => {
112+
vi.restoreAllMocks();
113+
});
114+
115+
it("parses object params from JSON strings", async () => {
116+
const op: Operation = {
117+
id: "createVideos",
118+
method: "POST",
119+
path: "/videos",
120+
summary: "Create video",
121+
description: "",
122+
params: [
123+
{ name: "name", in: "body", type: "string", required: true, description: "" },
124+
{ name: "content", in: "body", type: "object", required: true, description: "" },
125+
],
126+
bodyRequired: true,
127+
security: [],
128+
};
129+
130+
let capturedBody: unknown;
131+
vi.stubGlobal("fetch", vi.fn().mockImplementation((_url: string, init: RequestInit) => {
132+
capturedBody = JSON.parse(init.body as string);
133+
return Promise.resolve({
134+
status: 200,
135+
statusText: "OK",
136+
headers: new Map([["content-type", "application/json"]]),
137+
text: () => Promise.resolve("{}"),
138+
});
139+
}));
140+
141+
const spec = await loadSpec(FIXTURE);
142+
const program = new Command();
143+
program.exitOverride();
144+
buildCommands(program, [{ tag: "Videos", description: "", operations: [op] }], config, spec);
145+
146+
await program.parseAsync(["node", "test", "Videos", "create", "--name", "Test", "--content", '{"text":"hello"}']);
147+
148+
expect(capturedBody).toEqual({ name: "Test", content: { text: "hello" } });
149+
150+
vi.unstubAllGlobals();
151+
});
152+
153+
it("parses array params from JSON strings", async () => {
154+
const op: Operation = {
155+
id: "createItem",
156+
method: "POST",
157+
path: "/items",
158+
summary: "Create items",
159+
description: "",
160+
params: [
161+
{ name: "tags", in: "body", type: "array", required: true, description: "" },
162+
],
163+
bodyRequired: true,
164+
security: [],
165+
};
166+
167+
let capturedBody: unknown;
168+
vi.stubGlobal("fetch", vi.fn().mockImplementation((_url: string, init: RequestInit) => {
169+
capturedBody = JSON.parse(init.body as string);
170+
return Promise.resolve({
171+
status: 200,
172+
statusText: "OK",
173+
headers: new Map([["content-type", "application/json"]]),
174+
text: () => Promise.resolve("{}"),
175+
});
176+
}));
177+
178+
const spec = await loadSpec(FIXTURE);
179+
const program = new Command();
180+
program.exitOverride();
181+
buildCommands(program, [{ tag: "Items", description: "", operations: [op] }], config, spec);
182+
183+
await program.parseAsync(["node", "test", "Items", "create", "--tags", '["a","b"]']);
184+
185+
expect(capturedBody).toEqual({ tags: ["a", "b"] });
186+
187+
vi.unstubAllGlobals();
188+
});
189+
190+
it("falls back to string when JSON parse fails", async () => {
191+
const op: Operation = {
192+
id: "createThings",
193+
method: "POST",
194+
path: "/things",
195+
summary: "Create thing",
196+
description: "",
197+
params: [
198+
{ name: "data", in: "body", type: "object", required: true, description: "" },
199+
],
200+
bodyRequired: true,
201+
security: [],
202+
};
203+
204+
let capturedBody: unknown;
205+
vi.stubGlobal("fetch", vi.fn().mockImplementation((_url: string, init: RequestInit) => {
206+
capturedBody = JSON.parse(init.body as string);
207+
return Promise.resolve({
208+
status: 200,
209+
statusText: "OK",
210+
headers: new Map([["content-type", "application/json"]]),
211+
text: () => Promise.resolve("{}"),
212+
});
213+
}));
214+
215+
const spec = await loadSpec(FIXTURE);
216+
const program = new Command();
217+
program.exitOverride();
218+
buildCommands(program, [{ tag: "Things", description: "", operations: [op] }], config, spec);
219+
220+
await program.parseAsync(["node", "test", "Things", "create", "--data", "not-json"]);
221+
222+
expect(capturedBody).toEqual({ data: "not-json" });
223+
224+
vi.unstubAllGlobals();
225+
});
226+
});

src/executor/commander-builder.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ function collectParams(
9595
case "boolean":
9696
params[p.name] = value === true || value === "true";
9797
break;
98+
case "object":
99+
case "array":
100+
if (typeof value === "string") {
101+
try {
102+
params[p.name] = JSON.parse(value);
103+
} catch {
104+
params[p.name] = value;
105+
}
106+
} else {
107+
params[p.name] = value;
108+
}
109+
break;
98110
default:
99111
params[p.name] = value;
100112
}

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ function buildDynamicCommands(
167167
params[p.name] = Number(opts[p.name]);
168168
} else if (p.type === "boolean") {
169169
params[p.name] = opts[p.name] === true || opts[p.name] === "true";
170+
} else if ((p.type === "object" || p.type === "array") && typeof opts[p.name] === "string") {
171+
try {
172+
params[p.name] = JSON.parse(opts[p.name] as string);
173+
} catch {
174+
params[p.name] = opts[p.name];
175+
}
170176
} else {
171177
params[p.name] = opts[p.name];
172178
}

0 commit comments

Comments
 (0)