specli 0.0.1 → 0.0.2
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.
- package/README.md +83 -49
- package/cli.ts +4 -10
- package/package.json +8 -2
- package/src/cli/compile.ts +5 -28
- package/src/cli/derive-name.ts +2 -2
- package/src/cli/exec.ts +1 -1
- package/src/cli/main.ts +12 -27
- package/src/cli/runtime/auth/resolve.ts +10 -2
- package/src/cli/runtime/body-flags.ts +176 -0
- package/src/cli/runtime/execute.ts +17 -22
- package/src/cli/runtime/generated.ts +23 -54
- package/src/cli/runtime/profile/secrets.ts +1 -1
- package/src/cli/runtime/profile/store.ts +1 -1
- package/src/cli/runtime/request.ts +48 -80
- package/src/cli/stable-json.ts +2 -2
- package/src/compiled.ts +13 -15
- package/src/macros/env.ts +0 -4
- package/CLAUDE.md +0 -111
- package/PLAN.md +0 -274
- package/biome.jsonc +0 -1
- package/bun.lock +0 -98
- package/fixtures/openapi-array-items.json +0 -22
- package/fixtures/openapi-auth.json +0 -34
- package/fixtures/openapi-body.json +0 -41
- package/fixtures/openapi-collision.json +0 -21
- package/fixtures/openapi-oauth.json +0 -54
- package/fixtures/openapi-servers.json +0 -35
- package/fixtures/openapi.json +0 -87
- package/scripts/smoke-specs.ts +0 -64
- package/src/cli/auth-requirements.test.ts +0 -27
- package/src/cli/auth-schemes.test.ts +0 -66
- package/src/cli/capabilities.test.ts +0 -94
- package/src/cli/command-id.test.ts +0 -32
- package/src/cli/command-model.test.ts +0 -44
- package/src/cli/naming.test.ts +0 -86
- package/src/cli/operations.test.ts +0 -57
- package/src/cli/params.test.ts +0 -70
- package/src/cli/positional.test.ts +0 -65
- package/src/cli/request-body.test.ts +0 -35
- package/src/cli/runtime/request.test.ts +0 -153
- package/src/cli/server.test.ts +0 -35
- package/tsconfig.json +0 -29
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"openapi": "3.0.3",
|
|
3
|
-
"info": {
|
|
4
|
-
"title": "OAuth API",
|
|
5
|
-
"version": "1.0.0"
|
|
6
|
-
},
|
|
7
|
-
"servers": [{ "url": "https://api.example.com" }],
|
|
8
|
-
"security": [
|
|
9
|
-
{
|
|
10
|
-
"oauth": ["read:ping"]
|
|
11
|
-
},
|
|
12
|
-
{}
|
|
13
|
-
],
|
|
14
|
-
"components": {
|
|
15
|
-
"securitySchemes": {
|
|
16
|
-
"oauth": {
|
|
17
|
-
"type": "oauth2",
|
|
18
|
-
"flows": {
|
|
19
|
-
"authorizationCode": {
|
|
20
|
-
"authorizationUrl": "https://example.com/oauth/authorize",
|
|
21
|
-
"tokenUrl": "https://example.com/oauth/token",
|
|
22
|
-
"scopes": {
|
|
23
|
-
"read:ping": "read ping"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"clientCredentials": {
|
|
27
|
-
"tokenUrl": "https://example.com/oauth/token",
|
|
28
|
-
"scopes": {
|
|
29
|
-
"read:ping": "read ping",
|
|
30
|
-
"write:ping": "write ping"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"paths": {
|
|
38
|
-
"/ping": {
|
|
39
|
-
"get": {
|
|
40
|
-
"operationId": "Ping.Get",
|
|
41
|
-
"security": [
|
|
42
|
-
{
|
|
43
|
-
"oauth": ["read:ping"]
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"responses": {
|
|
47
|
-
"200": {
|
|
48
|
-
"description": "OK"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"openapi": "3.0.3",
|
|
3
|
-
"info": {
|
|
4
|
-
"title": "Servers API",
|
|
5
|
-
"version": "1.0.0"
|
|
6
|
-
},
|
|
7
|
-
"servers": [
|
|
8
|
-
{
|
|
9
|
-
"url": "https://{region}.api.example.com/{basePath}",
|
|
10
|
-
"description": "Regional server",
|
|
11
|
-
"variables": {
|
|
12
|
-
"region": {
|
|
13
|
-
"default": "us",
|
|
14
|
-
"enum": ["us", "eu"],
|
|
15
|
-
"description": "Region code"
|
|
16
|
-
},
|
|
17
|
-
"basePath": {
|
|
18
|
-
"default": "v1"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
"paths": {
|
|
24
|
-
"/ping": {
|
|
25
|
-
"get": {
|
|
26
|
-
"operationId": "Ping.Get",
|
|
27
|
-
"responses": {
|
|
28
|
-
"200": {
|
|
29
|
-
"description": "OK"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
package/fixtures/openapi.json
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"openapi": "3.0.3",
|
|
3
|
-
"info": {
|
|
4
|
-
"title": "Contacts API",
|
|
5
|
-
"version": "1.0.0"
|
|
6
|
-
},
|
|
7
|
-
"servers": [
|
|
8
|
-
{
|
|
9
|
-
"url": "https://api.example.com"
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"paths": {
|
|
13
|
-
"/contacts": {
|
|
14
|
-
"get": {
|
|
15
|
-
"operationId": "Contacts.List",
|
|
16
|
-
"tags": ["Contacts"],
|
|
17
|
-
"summary": "List contacts",
|
|
18
|
-
"parameters": [
|
|
19
|
-
{
|
|
20
|
-
"in": "query",
|
|
21
|
-
"name": "limit",
|
|
22
|
-
"schema": { "type": "integer", "minimum": 1, "maximum": 100 },
|
|
23
|
-
"description": "Max items to return"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"in": "query",
|
|
27
|
-
"name": "tag",
|
|
28
|
-
"schema": { "type": "array", "items": { "type": "string" } },
|
|
29
|
-
"description": "Filter by tags"
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"responses": {
|
|
33
|
-
"200": {
|
|
34
|
-
"description": "OK",
|
|
35
|
-
"content": {
|
|
36
|
-
"application/json": {
|
|
37
|
-
"schema": {
|
|
38
|
-
"type": "array",
|
|
39
|
-
"items": {
|
|
40
|
-
"type": "object",
|
|
41
|
-
"properties": {
|
|
42
|
-
"id": { "type": "string" },
|
|
43
|
-
"name": { "type": "string" }
|
|
44
|
-
},
|
|
45
|
-
"required": ["id", "name"]
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"/contacts/{id}": {
|
|
55
|
-
"get": {
|
|
56
|
-
"operationId": "Contacts.Get",
|
|
57
|
-
"tags": ["Contacts"],
|
|
58
|
-
"summary": "Get contact",
|
|
59
|
-
"parameters": [
|
|
60
|
-
{
|
|
61
|
-
"in": "path",
|
|
62
|
-
"name": "id",
|
|
63
|
-
"required": true,
|
|
64
|
-
"schema": { "type": "string" }
|
|
65
|
-
}
|
|
66
|
-
],
|
|
67
|
-
"responses": {
|
|
68
|
-
"200": {
|
|
69
|
-
"description": "OK",
|
|
70
|
-
"content": {
|
|
71
|
-
"application/json": {
|
|
72
|
-
"schema": {
|
|
73
|
-
"type": "object",
|
|
74
|
-
"properties": {
|
|
75
|
-
"id": { "type": "string" },
|
|
76
|
-
"name": { "type": "string" }
|
|
77
|
-
},
|
|
78
|
-
"required": ["id", "name"]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
package/scripts/smoke-specs.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
const specs = [
|
|
4
|
-
{
|
|
5
|
-
name: "vercel",
|
|
6
|
-
url: "https://api.vercel.com/copper/_openapi.json",
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
name: "digitalocean",
|
|
10
|
-
url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
|
|
11
|
-
},
|
|
12
|
-
// {
|
|
13
|
-
// name: "stripe",
|
|
14
|
-
// url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml",
|
|
15
|
-
// },
|
|
16
|
-
// NOTE: OpenAI spec is not reliably fetchable via raw.githubusercontent.com
|
|
17
|
-
// in some environments (intermittent 404). Keep it out of the default smoke
|
|
18
|
-
// list to avoid flaky CI.
|
|
19
|
-
// {
|
|
20
|
-
// name: "openai",
|
|
21
|
-
// url: "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
|
|
22
|
-
// },
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
|
-
type Result = {
|
|
26
|
-
spec: string;
|
|
27
|
-
ok: boolean;
|
|
28
|
-
ms: number;
|
|
29
|
-
error?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const results: Result[] = [];
|
|
33
|
-
|
|
34
|
-
for (const spec of specs) {
|
|
35
|
-
const start = performance.now();
|
|
36
|
-
try {
|
|
37
|
-
console.log(spec.url);
|
|
38
|
-
await Bun.$`bun ./cli.ts exec ${spec.url} __schema --json --min > /dev/null`;
|
|
39
|
-
results.push({
|
|
40
|
-
spec: spec.name,
|
|
41
|
-
ok: true,
|
|
42
|
-
ms: Math.round(performance.now() - start),
|
|
43
|
-
});
|
|
44
|
-
} catch (err) {
|
|
45
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
-
results.push({
|
|
47
|
-
spec: spec.name,
|
|
48
|
-
ok: false,
|
|
49
|
-
ms: Math.round(performance.now() - start),
|
|
50
|
-
error: message,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const allOk = results.every((r) => r.ok);
|
|
56
|
-
for (const r of results) {
|
|
57
|
-
if (r.ok) {
|
|
58
|
-
process.stdout.write(`ok ${r.spec} (${r.ms}ms)\n`);
|
|
59
|
-
} else {
|
|
60
|
-
process.stdout.write(`fail ${r.spec} (${r.ms}ms)\n${r.error}\n`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
process.exitCode = allOk ? 0 : 1;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { summarizeAuth } from "./auth-requirements.ts";
|
|
4
|
-
import type { AuthScheme } from "./auth-schemes.ts";
|
|
5
|
-
|
|
6
|
-
describe("summarizeAuth", () => {
|
|
7
|
-
test("uses operation-level security when present", () => {
|
|
8
|
-
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
9
|
-
|
|
10
|
-
const summary = summarizeAuth(
|
|
11
|
-
[{ oauth: ["read:ping"] }],
|
|
12
|
-
[{ oauth: ["read:other"] }],
|
|
13
|
-
schemes,
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
expect(summary.alternatives).toEqual([
|
|
17
|
-
[{ key: "oauth", scopes: ["read:ping"] }],
|
|
18
|
-
]);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("empty operation security disables auth", () => {
|
|
22
|
-
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
23
|
-
|
|
24
|
-
const summary = summarizeAuth([], [{ oauth: ["read:other"] }], schemes);
|
|
25
|
-
expect(summary.alternatives).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { listAuthSchemes } from "./auth-schemes.ts";
|
|
4
|
-
import type { OpenApiDoc } from "./types.ts";
|
|
5
|
-
|
|
6
|
-
describe("listAuthSchemes", () => {
|
|
7
|
-
test("parses bearer + apiKey", () => {
|
|
8
|
-
const doc: OpenApiDoc = {
|
|
9
|
-
openapi: "3.0.3",
|
|
10
|
-
components: {
|
|
11
|
-
securitySchemes: {
|
|
12
|
-
bearerAuth: {
|
|
13
|
-
type: "http",
|
|
14
|
-
scheme: "bearer",
|
|
15
|
-
bearerFormat: "JWT",
|
|
16
|
-
},
|
|
17
|
-
apiKeyAuth: {
|
|
18
|
-
type: "apiKey",
|
|
19
|
-
in: "header",
|
|
20
|
-
name: "X-API-Key",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const schemes = listAuthSchemes(doc);
|
|
27
|
-
expect(schemes).toHaveLength(2);
|
|
28
|
-
|
|
29
|
-
const bearer = schemes.find((s) => s.key === "bearerAuth");
|
|
30
|
-
expect(bearer?.kind).toBe("http-bearer");
|
|
31
|
-
|
|
32
|
-
const apiKey = schemes.find((s) => s.key === "apiKeyAuth");
|
|
33
|
-
expect(apiKey?.kind).toBe("api-key");
|
|
34
|
-
expect(apiKey?.in).toBe("header");
|
|
35
|
-
expect(apiKey?.name).toBe("X-API-Key");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("parses oauth2 flows", () => {
|
|
39
|
-
const doc = {
|
|
40
|
-
openapi: "3.0.3",
|
|
41
|
-
components: {
|
|
42
|
-
securitySchemes: {
|
|
43
|
-
oauth: {
|
|
44
|
-
type: "oauth2",
|
|
45
|
-
flows: {
|
|
46
|
-
clientCredentials: {
|
|
47
|
-
tokenUrl: "https://example.com/oauth/token",
|
|
48
|
-
scopes: {
|
|
49
|
-
"read:ping": "read ping",
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
} as const;
|
|
57
|
-
|
|
58
|
-
const schemes = listAuthSchemes(doc);
|
|
59
|
-
const oauth = schemes.find((s) => s.key === "oauth");
|
|
60
|
-
expect(oauth?.kind).toBe("oauth2");
|
|
61
|
-
expect(oauth?.oauthFlows?.clientCredentials?.tokenUrl).toBe(
|
|
62
|
-
"https://example.com/oauth/token",
|
|
63
|
-
);
|
|
64
|
-
expect(oauth?.oauthFlows?.clientCredentials?.scopes).toEqual(["read:ping"]);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { AuthScheme } from "./auth-schemes.ts";
|
|
3
|
-
import { deriveCapabilities } from "./capabilities.ts";
|
|
4
|
-
import type { CommandModel } from "./command-model.ts";
|
|
5
|
-
import type { ServerInfo } from "./server.ts";
|
|
6
|
-
import type { NormalizedOperation, OpenApiDoc } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
describe("deriveCapabilities", () => {
|
|
9
|
-
test("reports requestBody + server vars", () => {
|
|
10
|
-
const doc: OpenApiDoc = {
|
|
11
|
-
openapi: "3.0.3",
|
|
12
|
-
security: [{ bearerAuth: [] }],
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const servers: ServerInfo[] = [
|
|
16
|
-
{
|
|
17
|
-
url: "https://{region}.api.example.com",
|
|
18
|
-
variables: [],
|
|
19
|
-
variableNames: ["region"],
|
|
20
|
-
},
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const authSchemes: AuthScheme[] = [
|
|
24
|
-
{ key: "bearerAuth", kind: "http-bearer" },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const operations: NormalizedOperation[] = [
|
|
28
|
-
{
|
|
29
|
-
key: "POST /contacts",
|
|
30
|
-
method: "POST",
|
|
31
|
-
path: "/contacts",
|
|
32
|
-
tags: [],
|
|
33
|
-
parameters: [],
|
|
34
|
-
requestBody: {
|
|
35
|
-
required: true,
|
|
36
|
-
contentTypes: ["application/json"],
|
|
37
|
-
schemasByContentType: { "application/json": { type: "object" } },
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
const commands: CommandModel = {
|
|
43
|
-
resources: [
|
|
44
|
-
{
|
|
45
|
-
resource: "contacts",
|
|
46
|
-
actions: [
|
|
47
|
-
{
|
|
48
|
-
id: "x",
|
|
49
|
-
key: "POST /contacts",
|
|
50
|
-
action: "create",
|
|
51
|
-
pathArgs: [],
|
|
52
|
-
method: "POST",
|
|
53
|
-
path: "/contacts",
|
|
54
|
-
tags: [],
|
|
55
|
-
style: "rest",
|
|
56
|
-
positionals: [],
|
|
57
|
-
flags: [],
|
|
58
|
-
params: [],
|
|
59
|
-
auth: { alternatives: [] },
|
|
60
|
-
requestBody: {
|
|
61
|
-
required: true,
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
contentType: "application/json",
|
|
65
|
-
required: true,
|
|
66
|
-
schemaType: "object",
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
hasJson: true,
|
|
70
|
-
hasFormUrlEncoded: false,
|
|
71
|
-
hasMultipart: false,
|
|
72
|
-
bodyFlags: ["--data", "--file"],
|
|
73
|
-
preferredContentType: "application/json",
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const caps = deriveCapabilities({
|
|
82
|
-
doc,
|
|
83
|
-
servers,
|
|
84
|
-
authSchemes,
|
|
85
|
-
operations,
|
|
86
|
-
commands,
|
|
87
|
-
});
|
|
88
|
-
expect(caps.servers.hasVariables).toBe(true);
|
|
89
|
-
expect(caps.operations.hasRequestBodies).toBe(true);
|
|
90
|
-
expect(caps.commands.hasRequestBodies).toBe(true);
|
|
91
|
-
expect(caps.auth.hasSecurityRequirements).toBe(true);
|
|
92
|
-
expect(caps.auth.kinds).toEqual(["http-bearer"]);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { buildCommandId } from "./command-id.ts";
|
|
4
|
-
|
|
5
|
-
describe("buildCommandId", () => {
|
|
6
|
-
test("includes spec/resource/action/op", () => {
|
|
7
|
-
expect(
|
|
8
|
-
buildCommandId({
|
|
9
|
-
specId: "contacts-api",
|
|
10
|
-
resource: "contacts",
|
|
11
|
-
action: "get",
|
|
12
|
-
operationKey: "GET /contacts/{id}",
|
|
13
|
-
}),
|
|
14
|
-
).toBe("contacts-api:contacts:get:get-contacts-id");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("disambiguates by operationKey", () => {
|
|
18
|
-
const a = buildCommandId({
|
|
19
|
-
specId: "x",
|
|
20
|
-
resource: "contacts",
|
|
21
|
-
action: "list",
|
|
22
|
-
operationKey: "GET /contacts",
|
|
23
|
-
});
|
|
24
|
-
const b = buildCommandId({
|
|
25
|
-
specId: "x",
|
|
26
|
-
resource: "contacts",
|
|
27
|
-
action: "list",
|
|
28
|
-
operationKey: "GET /contacts/search",
|
|
29
|
-
});
|
|
30
|
-
expect(a).not.toBe(b);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { buildCommandModel } from "./command-model.ts";
|
|
4
|
-
import type { PlannedOperation } from "./naming.ts";
|
|
5
|
-
|
|
6
|
-
describe("buildCommandModel", () => {
|
|
7
|
-
test("groups operations by resource", () => {
|
|
8
|
-
const planned: PlannedOperation[] = [
|
|
9
|
-
{
|
|
10
|
-
key: "GET /contacts",
|
|
11
|
-
method: "GET",
|
|
12
|
-
path: "/contacts",
|
|
13
|
-
tags: ["Contacts"],
|
|
14
|
-
parameters: [],
|
|
15
|
-
resource: "contacts",
|
|
16
|
-
action: "list",
|
|
17
|
-
canonicalAction: "list",
|
|
18
|
-
pathArgs: [],
|
|
19
|
-
style: "rest",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
key: "GET /contacts/{id}",
|
|
23
|
-
method: "GET",
|
|
24
|
-
path: "/contacts/{id}",
|
|
25
|
-
tags: ["Contacts"],
|
|
26
|
-
parameters: [],
|
|
27
|
-
resource: "contacts",
|
|
28
|
-
action: "get",
|
|
29
|
-
canonicalAction: "get",
|
|
30
|
-
pathArgs: ["id"],
|
|
31
|
-
style: "rest",
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const model = buildCommandModel(planned, { specId: "contacts-api" });
|
|
36
|
-
expect(model.resources).toHaveLength(1);
|
|
37
|
-
expect(model.resources[0]?.resource).toBe("contacts");
|
|
38
|
-
expect(model.resources[0]?.actions).toHaveLength(2);
|
|
39
|
-
expect(model.resources[0]?.actions.map((a) => a.action)).toEqual([
|
|
40
|
-
"get",
|
|
41
|
-
"list",
|
|
42
|
-
]);
|
|
43
|
-
});
|
|
44
|
-
});
|
package/src/cli/naming.test.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { planOperation } from "./naming.ts";
|
|
3
|
-
import type { NormalizedOperation } from "./types.ts";
|
|
4
|
-
|
|
5
|
-
describe("planOperation", () => {
|
|
6
|
-
test("REST: GET /contacts -> contacts list", () => {
|
|
7
|
-
const op: NormalizedOperation = {
|
|
8
|
-
key: "GET /contacts",
|
|
9
|
-
method: "GET",
|
|
10
|
-
path: "/contacts",
|
|
11
|
-
operationId: "Contacts.List",
|
|
12
|
-
tags: ["Contacts"],
|
|
13
|
-
parameters: [],
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const planned = planOperation(op);
|
|
17
|
-
expect(planned.style).toBe("rest");
|
|
18
|
-
expect(planned.resource).toBe("contacts");
|
|
19
|
-
expect(planned.action).toBe("list");
|
|
20
|
-
expect(planned.pathArgs).toEqual([]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("REST: singleton /ping stays ping and prefers operationId action", () => {
|
|
24
|
-
const op: NormalizedOperation = {
|
|
25
|
-
key: "GET /ping",
|
|
26
|
-
method: "GET",
|
|
27
|
-
path: "/ping",
|
|
28
|
-
operationId: "Ping.Get",
|
|
29
|
-
tags: [],
|
|
30
|
-
parameters: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const planned = planOperation(op);
|
|
34
|
-
expect(planned.style).toBe("rest");
|
|
35
|
-
expect(planned.resource).toBe("ping");
|
|
36
|
-
expect(planned.action).toBe("get");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("REST: singular path pluralizes to contacts", () => {
|
|
40
|
-
const op: NormalizedOperation = {
|
|
41
|
-
key: "GET /contact/{id}",
|
|
42
|
-
method: "GET",
|
|
43
|
-
path: "/contact/{id}",
|
|
44
|
-
tags: [],
|
|
45
|
-
parameters: [],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const planned = planOperation(op);
|
|
49
|
-
expect(planned.style).toBe("rest");
|
|
50
|
-
expect(planned.resource).toBe("contacts");
|
|
51
|
-
expect(planned.action).toBe("get");
|
|
52
|
-
expect(planned.pathArgs).toEqual(["id"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("RPC: POST /Contacts.List -> contacts list", () => {
|
|
56
|
-
const op: NormalizedOperation = {
|
|
57
|
-
key: "POST /Contacts.List",
|
|
58
|
-
method: "POST",
|
|
59
|
-
path: "/Contacts.List",
|
|
60
|
-
operationId: "Contacts.List",
|
|
61
|
-
tags: [],
|
|
62
|
-
parameters: [],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const planned = planOperation(op);
|
|
66
|
-
expect(planned.style).toBe("rpc");
|
|
67
|
-
expect(planned.resource).toBe("contacts");
|
|
68
|
-
expect(planned.action).toBe("list");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("RPC: Retrieve canonicalizes to get", () => {
|
|
72
|
-
const op: NormalizedOperation = {
|
|
73
|
-
key: "POST /Contacts.Retrieve",
|
|
74
|
-
method: "POST",
|
|
75
|
-
path: "/Contacts.Retrieve",
|
|
76
|
-
operationId: "Contacts.Retrieve",
|
|
77
|
-
tags: [],
|
|
78
|
-
parameters: [],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const planned = planOperation(op);
|
|
82
|
-
expect(planned.style).toBe("rpc");
|
|
83
|
-
expect(planned.resource).toBe("contacts");
|
|
84
|
-
expect(planned.action).toBe("get");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { indexOperations } from "./operations.ts";
|
|
4
|
-
import type { OpenApiDoc } from "./types.ts";
|
|
5
|
-
|
|
6
|
-
describe("indexOperations", () => {
|
|
7
|
-
test("indexes basic operations", () => {
|
|
8
|
-
const doc: OpenApiDoc = {
|
|
9
|
-
openapi: "3.0.3",
|
|
10
|
-
paths: {
|
|
11
|
-
"/contacts": {
|
|
12
|
-
get: {
|
|
13
|
-
operationId: "Contacts.List",
|
|
14
|
-
tags: ["Contacts"],
|
|
15
|
-
parameters: [
|
|
16
|
-
{
|
|
17
|
-
in: "query",
|
|
18
|
-
name: "limit",
|
|
19
|
-
schema: { type: "integer" },
|
|
20
|
-
},
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
"/contacts/{id}": {
|
|
25
|
-
get: {
|
|
26
|
-
operationId: "Contacts.Get",
|
|
27
|
-
tags: ["Contacts"],
|
|
28
|
-
parameters: [
|
|
29
|
-
{
|
|
30
|
-
in: "path",
|
|
31
|
-
name: "id",
|
|
32
|
-
required: true,
|
|
33
|
-
schema: { type: "string" },
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const ops = indexOperations(doc);
|
|
42
|
-
expect(ops).toHaveLength(2);
|
|
43
|
-
|
|
44
|
-
expect(ops[0]?.key).toBe("GET /contacts");
|
|
45
|
-
expect(ops[0]?.path).toBe("/contacts");
|
|
46
|
-
expect(ops[0]?.method).toBe("GET");
|
|
47
|
-
expect(ops[0]?.parameters).toHaveLength(1);
|
|
48
|
-
expect(ops[0]?.parameters[0]?.in).toBe("query");
|
|
49
|
-
|
|
50
|
-
expect(ops[1]?.key).toBe("GET /contacts/{id}");
|
|
51
|
-
expect(ops[1]?.path).toBe("/contacts/{id}");
|
|
52
|
-
expect(ops[1]?.method).toBe("GET");
|
|
53
|
-
expect(ops[1]?.parameters).toHaveLength(1);
|
|
54
|
-
expect(ops[1]?.parameters[0]?.in).toBe("path");
|
|
55
|
-
expect(ops[1]?.parameters[0]?.required).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
});
|