Skip to content

Commit 3453d51

Browse files
Merge pull request #5 from veged/fix/nested-body-json-flags
fix: parse nested JSON values for body flags
2 parents ffcefa0 + 3936aa7 commit 3453d51

2 files changed

Lines changed: 194 additions & 2 deletions

File tree

src/cli.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ async function runApiCommand(
154154
const headers = buildHeaders(profile);
155155

156156
const knownOptionNames = new Set(command.options.map((o) => o.name));
157-
const body: Record<string, string> = {};
157+
const body: Record<string, unknown> = {};
158158

159159
Object.keys(flags).forEach((key) => {
160160
if (!knownOptionNames.has(key)) {
161-
body[key] = flags[key];
161+
body[key] = parseBodyFlagValue(flags[key]);
162162
}
163163
});
164164

@@ -179,6 +179,24 @@ async function runApiCommand(
179179
stdout(`${JSON.stringify(response.data, null, 2)}\n`);
180180
}
181181

182+
function parseBodyFlagValue(value: string): unknown {
183+
const trimmed = value.trim();
184+
185+
if (trimmed === "true") return true;
186+
if (trimmed === "false") return false;
187+
if (trimmed === "null") return null;
188+
189+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
190+
try {
191+
return JSON.parse(trimmed);
192+
} catch {
193+
throw new Error(`Invalid JSON body value: ${trimmed}`);
194+
}
195+
}
196+
197+
return value;
198+
}
199+
182200
function parseArgs(args: string[]): { flags: Record<string, string>; positional: string[] } {
183201
const flags: Record<string, string> = {};
184202
const positional: string[] = [];

tests/cli.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,180 @@ describe("cli", () => {
576576
expect(out).toContain('"ok": true');
577577
});
578578

579+
// --- Body flag JSON parsing tests ---
580+
581+
function createPostApiDeps() {
582+
const localDir = `${cwd}/.ocli`;
583+
const profilesPath = `${localDir}/profiles.ini`;
584+
const cachePath = `${localDir}/specs/body-api.json`;
585+
586+
const spec = {
587+
openapi: "3.0.0",
588+
paths: {
589+
"/{org_slug}/{repo_slug}/ci/workflows/{workflow_name}/trigger": {
590+
post: {
591+
summary: "Trigger workflow",
592+
parameters: [
593+
{ name: "org_slug", in: "path", required: true, schema: { type: "string" } },
594+
{ name: "repo_slug", in: "path", required: true, schema: { type: "string" } },
595+
{ name: "workflow_name", in: "path", required: true, schema: { type: "string" } },
596+
],
597+
},
598+
},
599+
},
600+
};
601+
602+
const iniContent = [
603+
"[body-api]",
604+
"api_base_url = https://api.example.com",
605+
"api_basic_auth = ",
606+
"api_bearer_token = tok",
607+
"openapi_spec_source = /spec.json",
608+
`openapi_spec_cache = ${cachePath}`,
609+
"include_endpoints = ",
610+
"exclude_endpoints = ",
611+
"",
612+
].join("\n");
613+
614+
const capturedConfigs: unknown[] = [];
615+
const fakeHttpClient: HttpClient = {
616+
request: async (config: any) => {
617+
capturedConfigs.push(config);
618+
return { status: 200, statusText: "OK", headers: {}, config, data: { ok: true } };
619+
},
620+
};
621+
622+
const { profileStore, openapiLoader } = createCliDeps(cwd, homeDir, {
623+
[profilesPath]: iniContent,
624+
[cachePath]: JSON.stringify(spec),
625+
[`${localDir}/current`]: "body-api",
626+
});
627+
628+
return { profileStore, openapiLoader, fakeHttpClient, capturedConfigs };
629+
}
630+
631+
it("parses JSON object body flags before sending request", async () => {
632+
const { profileStore, openapiLoader, fakeHttpClient, capturedConfigs } = createPostApiDeps();
633+
634+
await run(
635+
[
636+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
637+
"--org_slug", "myorg",
638+
"--repo_slug", "myrepo",
639+
"--workflow_name", "deploy",
640+
"--revision", "main",
641+
"--workflow_revision", "main",
642+
"--input", '{"values":[{"name":"FOO","value":"bar"}]}',
643+
],
644+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
645+
);
646+
647+
expect(capturedConfigs).toHaveLength(1);
648+
const config = capturedConfigs[0] as { data: Record<string, unknown> };
649+
expect(config.data).toEqual({
650+
revision: "main",
651+
workflow_revision: "main",
652+
input: {
653+
values: [{ name: "FOO", value: "bar" }],
654+
},
655+
});
656+
});
657+
658+
it("parses boolean body flags before sending request", async () => {
659+
const { profileStore, openapiLoader, fakeHttpClient, capturedConfigs } = createPostApiDeps();
660+
661+
await run(
662+
[
663+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
664+
"--org_slug", "myorg",
665+
"--repo_slug", "myrepo",
666+
"--workflow_name", "deploy",
667+
"--shared", "true",
668+
"--draft", "false",
669+
],
670+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
671+
);
672+
673+
expect(capturedConfigs).toHaveLength(1);
674+
const config = capturedConfigs[0] as { data: Record<string, unknown> };
675+
expect(config.data.shared).toBe(true);
676+
expect(config.data.draft).toBe(false);
677+
});
678+
679+
it("parses null body flags before sending request", async () => {
680+
const { profileStore, openapiLoader, fakeHttpClient, capturedConfigs } = createPostApiDeps();
681+
682+
await run(
683+
[
684+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
685+
"--org_slug", "myorg",
686+
"--repo_slug", "myrepo",
687+
"--workflow_name", "deploy",
688+
"--description", "null",
689+
],
690+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
691+
);
692+
693+
expect(capturedConfigs).toHaveLength(1);
694+
const config = capturedConfigs[0] as { data: Record<string, unknown> };
695+
expect(config.data.description).toBeNull();
696+
});
697+
698+
it("parses JSON array body flags before sending request", async () => {
699+
const { profileStore, openapiLoader, fakeHttpClient, capturedConfigs } = createPostApiDeps();
700+
701+
await run(
702+
[
703+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
704+
"--org_slug", "myorg",
705+
"--repo_slug", "myrepo",
706+
"--workflow_name", "deploy",
707+
"--tags", '["ci","deploy"]',
708+
],
709+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
710+
);
711+
712+
expect(capturedConfigs).toHaveLength(1);
713+
const config = capturedConfigs[0] as { data: Record<string, unknown> };
714+
expect(config.data.tags).toEqual(["ci", "deploy"]);
715+
});
716+
717+
it("preserves plain string body flags", async () => {
718+
const { profileStore, openapiLoader, fakeHttpClient, capturedConfigs } = createPostApiDeps();
719+
720+
await run(
721+
[
722+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
723+
"--org_slug", "myorg",
724+
"--repo_slug", "myrepo",
725+
"--workflow_name", "deploy",
726+
"--revision", "main",
727+
],
728+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
729+
);
730+
731+
expect(capturedConfigs).toHaveLength(1);
732+
const config = capturedConfigs[0] as { data: Record<string, unknown> };
733+
expect(config.data.revision).toBe("main");
734+
});
735+
736+
it("throws on invalid JSON-like body flags", async () => {
737+
const { profileStore, openapiLoader, fakeHttpClient } = createPostApiDeps();
738+
739+
await expect(
740+
run(
741+
[
742+
"org_slug_repo_slug_ci_workflows_workflow_name_trigger",
743+
"--org_slug", "myorg",
744+
"--repo_slug", "myrepo",
745+
"--workflow_name", "deploy",
746+
"--input", '{"values":',
747+
],
748+
{ cwd, profileStore, openapiLoader, httpClient: fakeHttpClient, stdout: () => {} }
749+
)
750+
).rejects.toThrow("Invalid JSON body value");
751+
});
752+
579753
it("sends custom headers from profile in API requests", async () => {
580754
const localDir = `${cwd}/.ocli`;
581755
const profilesPath = `${localDir}/profiles.ini`;

0 commit comments

Comments
 (0)