toolcraft-openapi 0.0.27 → 0.0.34
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/dist/auth/bearer-token-auth.js +1 -1
- package/dist/bin/generate.d.ts +9 -1
- package/dist/bin/generate.js +65 -9
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +6 -0
- package/dist/generate.d.ts +20 -3
- package/dist/generate.js +604 -144
- package/dist/http.d.ts +17 -1
- package/dist/http.js +113 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -0
- package/dist/inspect-source.d.ts +10 -0
- package/dist/inspect-source.js +14 -0
- package/dist/inspect.d.ts +22 -0
- package/dist/inspect.js +171 -0
- package/dist/interpreter.js +6 -2
- package/dist/mock/fetch.js +28 -12
- package/dist/mock/forgeyard.d.ts +4 -0
- package/dist/mock/forgeyard.js +452 -0
- package/dist/mock.d.ts +1 -0
- package/dist/mock.js +1 -0
- package/dist/naming.d.ts +4 -1
- package/dist/naming.js +93 -7
- package/dist/network-error.js +29 -12
- package/dist/normalize-swagger.d.ts +2 -0
- package/dist/normalize-swagger.js +201 -0
- package/dist/render-inspection.d.ts +2 -0
- package/dist/render-inspection.js +130 -0
- package/dist/request-shape.d.ts +4 -0
- package/dist/request-shape.js +1 -0
- package/dist/runtime.js +7 -1
- package/dist/spec-source.js +7 -4
- package/node_modules/auth-store/dist/create-secret-store.js +6 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +1 -1
- package/node_modules/auth-store/dist/encrypted-file-store.js +32 -28
- package/node_modules/auth-store/dist/error-codes.d.ts +1 -0
- package/node_modules/auth-store/dist/error-codes.js +5 -0
- package/node_modules/auth-store/dist/keychain-store.js +23 -8
- package/node_modules/toolcraft-design/README.md +148 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/components.d.ts +1 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/components.js +12 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/index.d.ts +1 -1
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/index.js +1 -1
- package/node_modules/toolcraft-design/dist/components/catalog.d.ts +26 -0
- package/node_modules/toolcraft-design/dist/components/catalog.js +94 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/index.d.ts +2 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/index.js +1 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/footer.js +1 -2
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/output-pane.js +16 -19
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/stats-pane.js +1 -15
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/demo.js +2 -2
- package/node_modules/toolcraft-design/dist/explorer/theme.js +29 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/index.d.ts +7 -1
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/index.js +4 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/theme-detect.d.ts +1 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/theme-detect.js +28 -6
- package/node_modules/toolcraft-design/dist/internal/theme-state.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/internal/theme-state.js +35 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/theme.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/prompts/theme.js +15 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/frontmatter.js +50 -0
- package/node_modules/toolcraft-design/dist/tokens/brand.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/tokens/brand.js +5 -0
- package/node_modules/toolcraft-design/dist/tokens/colors.d.ts +37 -0
- package/node_modules/toolcraft-design/dist/tokens/colors.js +85 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/index.d.ts +2 -0
- package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/index.js +1 -0
- package/node_modules/toolcraft-design/package.json +42 -0
- package/package.json +8 -6
- package/node_modules/@poe-code/design-system/dist/explorer/theme.js +0 -97
- package/node_modules/@poe-code/design-system/dist/prompts/theme.js +0 -12
- package/node_modules/@poe-code/design-system/dist/tokens/colors.d.ts +0 -35
- package/node_modules/@poe-code/design-system/dist/tokens/colors.js +0 -34
- package/node_modules/@poe-code/design-system/package.json +0 -29
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/writer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/acp/writer.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/browser.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/browser.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/color.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/color.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/command-errors.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/command-errors.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/detail-card.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/detail-card.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/help-formatter-plain.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/help-formatter-plain.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/help-formatter.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/help-formatter.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/logger.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/logger.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/symbols.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/symbols.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/table.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/table.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/template.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/template.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/text.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/components/text.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/ansi.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/ansi.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/buffer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/buffer.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/border.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/border.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/footer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/output-pane.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/components/stats-pane.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/dashboard.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/dashboard.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/demo.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/demo.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/keymap.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/keymap.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/layout.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/layout.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/should-use-dashboard.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/should-use-dashboard.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/snapshot.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/snapshot.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/store.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/store.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/terminal-width.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/terminal-width.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/terminal.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/terminal.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/types.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/dashboard/types.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/actions.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/actions.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/demo.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/events.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/events.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/filter.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/filter.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/jobs.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/jobs.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/keymap.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/keymap.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/layout.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/layout.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/reducer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/reducer.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/detail.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/detail.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/footer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/footer.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/header.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/header.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/list.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/list.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/modal.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/modal.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/test-fixtures.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/test-fixtures.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/text.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/render/text.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/runtime.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/runtime.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/runtime.test-helpers.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/runtime.test-helpers.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/state.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/state.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/explorer/theme.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/color-support.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/color-support.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/output-format.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/output-format.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/strip-ansi.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/internal/strip-ansi.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/cancel.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/cancel.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/intro.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/intro.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/log.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/log.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/note.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/note.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/outro.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/outro.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/spinner.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/prompts/primitives/spinner.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/menu.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/menu.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/spinner.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/static/spinner.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/ast.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/ast.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/demo-content.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/demo-content.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/index.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/index.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/block.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/block.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/frontmatter.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/inline.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser/inline.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/parser.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/renderer.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/renderer.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/testing/theme-render-fixture.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/terminal-markdown/testing/theme-render-fixture.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/spacing.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/spacing.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/typography.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/typography.js +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/widths.d.ts +0 -0
- /package/node_modules/{@poe-code/design-system → toolcraft-design}/dist/tokens/widths.js +0 -0
package/dist/generate.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ToolcraftBugError, UserError } from "toolcraft";
|
|
2
|
-
import { METHOD_DEFAULTS, deriveNoun, deriveVerb, isIdentifierName, normalizeParamName, toCamelCase, toPascalCase } from "./naming.js";
|
|
2
|
+
import { METHOD_DEFAULTS, deriveDisambiguatedVerb, deriveNoun, derivePathDisambiguatedVerb, deriveVerb, isIdentifierName, normalizeNoun, normalizeParamName, toCamelCase, toPascalCase } from "./naming.js";
|
|
3
3
|
import { groupByNoun } from "./group-by-noun.js";
|
|
4
4
|
import { renderPreflightBlock, renderRequestShape } from "./interpreter.js";
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
import { normalizeOpenApiDocument } from "./normalize-swagger.js";
|
|
6
|
+
const HTTP_METHOD_ORDER = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
7
|
+
const UNSUPPORTED_HTTP_METHODS = ["trace"];
|
|
8
|
+
const METHODS_WITHOUT_REQUEST_BODY = new Set(["head", "options"]);
|
|
8
9
|
const SCHEMA_TYPE_TO_KIND = {
|
|
9
10
|
boolean: { kind: "boolean" },
|
|
10
11
|
integer: { kind: "number", jsonType: "integer" },
|
|
@@ -13,6 +14,7 @@ const SCHEMA_TYPE_TO_KIND = {
|
|
|
13
14
|
};
|
|
14
15
|
const NULL_HELPER_SUPPORT = {
|
|
15
16
|
body: { array: true, scalar: true },
|
|
17
|
+
header: { array: false, scalar: false },
|
|
16
18
|
path: { array: false, scalar: false },
|
|
17
19
|
// Query null already serializes as the existing empty-string wire encoding, so v1
|
|
18
20
|
// keeps null-helper flags body-only until a real query-null convention lands.
|
|
@@ -108,7 +110,10 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
108
110
|
}
|
|
109
111
|
];
|
|
110
112
|
export function generate(document, options) {
|
|
111
|
-
const
|
|
113
|
+
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
114
|
+
const commands = collectGeneratedCommands(normalizedDocument);
|
|
115
|
+
const brand = options.brand ?? "blue";
|
|
116
|
+
const label = normalizedDocument.info?.title ?? "Toolcraft";
|
|
112
117
|
return [
|
|
113
118
|
...commands.map((command) => ({
|
|
114
119
|
path: command.filePath,
|
|
@@ -117,18 +122,30 @@ export function generate(document, options) {
|
|
|
117
122
|
...command
|
|
118
123
|
})
|
|
119
124
|
})),
|
|
120
|
-
createIndexFile(commands)
|
|
125
|
+
createIndexFile(commands),
|
|
126
|
+
createCliFile({ brand, label })
|
|
121
127
|
];
|
|
122
128
|
}
|
|
123
129
|
export function collectGeneratedCommands(document) {
|
|
124
|
-
const
|
|
130
|
+
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
131
|
+
const paths = normalizedDocument.paths;
|
|
125
132
|
if (paths === undefined) {
|
|
126
133
|
throw new UserError('OpenAPI document must define a top-level "paths" object.');
|
|
127
134
|
}
|
|
128
|
-
const commands = collectOperations(paths).map((entry) => createGeneratedCommand(
|
|
135
|
+
const commands = collectOperations(paths).map((entry) => createGeneratedCommand(normalizedDocument, entry));
|
|
136
|
+
disambiguateCommandPaths(commands);
|
|
129
137
|
assertUniqueCommandPaths(commands);
|
|
130
138
|
return commands.slice().sort((left, right) => compareGeneratedCommandPaths(left, right));
|
|
131
139
|
}
|
|
140
|
+
export function collectGeneratedCommand(document, path, method) {
|
|
141
|
+
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
142
|
+
const pathItem = normalizedDocument.paths?.[path];
|
|
143
|
+
const operation = pathItem?.[method];
|
|
144
|
+
if (pathItem === undefined || operation === undefined) {
|
|
145
|
+
throw new ToolcraftBugError(`Cannot generate missing OpenAPI operation ${method.toUpperCase()} ${path}.`);
|
|
146
|
+
}
|
|
147
|
+
return createGeneratedCommand(normalizedDocument, { method, path, operation, pathItem });
|
|
148
|
+
}
|
|
132
149
|
function collectOperations(paths) {
|
|
133
150
|
return Object.entries(paths)
|
|
134
151
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
@@ -138,7 +155,7 @@ function collectOperations(paths) {
|
|
|
138
155
|
}
|
|
139
156
|
assertSupportedHttpMethods(path, pathItem);
|
|
140
157
|
return HTTP_METHOD_ORDER.flatMap((method) => {
|
|
141
|
-
const operation = pathItem
|
|
158
|
+
const operation = getOwnPathItemValue(pathItem, method);
|
|
142
159
|
if (operation === undefined) {
|
|
143
160
|
return [];
|
|
144
161
|
}
|
|
@@ -146,15 +163,20 @@ function collectOperations(paths) {
|
|
|
146
163
|
});
|
|
147
164
|
});
|
|
148
165
|
}
|
|
166
|
+
function getOwnPathItemValue(pathItem, key) {
|
|
167
|
+
return Object.prototype.hasOwnProperty.call(pathItem, key)
|
|
168
|
+
? pathItem[key]
|
|
169
|
+
: undefined;
|
|
170
|
+
}
|
|
149
171
|
function createGeneratedCommand(document, entry) {
|
|
150
172
|
const operation = expectOperation(document, entry.operation, entry.method, entry.path);
|
|
151
173
|
const operationId = operation.operationId ?? `${entry.method.toUpperCase()} ${entry.path}`;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
174
|
+
const operationBaseUrl = resolveOperationBaseUrl(operation, operationId);
|
|
175
|
+
const auth = getOperationAuthMode(document, operation, operationId);
|
|
176
|
+
const response = resolveSuccessResponse(document, operation, operationId);
|
|
177
|
+
const noun = createSafeGeneratedNoun(deriveNoun(operation, entry.path, operationId));
|
|
156
178
|
const verb = deriveVerb(entry.method, entry.path, operation, operationId, noun);
|
|
157
|
-
const collected = collectParams(document, entry, operation, operationId);
|
|
179
|
+
const collected = collectParams(document, entry, operation, operationId, auth);
|
|
158
180
|
const methodDefaults = METHOD_DEFAULTS[entry.method];
|
|
159
181
|
const exportName = `${toCamelCase(noun)}${toPascalCase(verb)}Command`;
|
|
160
182
|
const filePath = `${noun}/${verb}.ts`;
|
|
@@ -167,7 +189,13 @@ function createGeneratedCommand(document, entry) {
|
|
|
167
189
|
description: mergeCommandDescriptions(operation.description ?? operation.summary, collected.requestBodyDescription),
|
|
168
190
|
method: entry.method.toUpperCase(),
|
|
169
191
|
path: entry.path,
|
|
170
|
-
auth
|
|
192
|
+
auth,
|
|
193
|
+
responseMode: response.mode,
|
|
194
|
+
accept: response.accept,
|
|
195
|
+
baseUrl: operationBaseUrl,
|
|
196
|
+
bodyMode: collected.bodyMode,
|
|
197
|
+
contentType: collected.contentType,
|
|
198
|
+
multipartBinaryFields: collected.multipartBinaryFields,
|
|
171
199
|
confirm: methodDefaults?.confirm === true,
|
|
172
200
|
params: collected.params,
|
|
173
201
|
paramsSchemaOptions: collected.paramsSchemaOptions,
|
|
@@ -177,15 +205,26 @@ function createGeneratedCommand(document, entry) {
|
|
|
177
205
|
optionalSections: collected.optionalSections
|
|
178
206
|
};
|
|
179
207
|
}
|
|
180
|
-
function
|
|
181
|
-
if (operation.servers
|
|
182
|
-
|
|
208
|
+
function resolveOperationBaseUrl(operation, operationId) {
|
|
209
|
+
if (operation.servers === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const [server] = operation.servers;
|
|
213
|
+
if (operation.servers.length !== 1 || server === undefined || server.url.includes("{")) {
|
|
214
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} must define exactly one fixed per-operation server URL in v1.`);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return new URL(server.url).toString().replace(/\/$/, "");
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} defines invalid per-operation server URL ${JSON.stringify(server.url)}.`);
|
|
183
221
|
}
|
|
184
222
|
}
|
|
185
|
-
function collectParams(document, entry, operation, operationId) {
|
|
186
|
-
const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId);
|
|
223
|
+
function collectParams(document, entry, operation, operationId, auth) {
|
|
224
|
+
const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId, auth);
|
|
187
225
|
const requestBodyParams = collectRequestBodyParams(document, operation, operationId, entry.method);
|
|
188
|
-
const
|
|
226
|
+
const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...TRANSPORT_PARAMS].map((param) => param.paramName)));
|
|
227
|
+
const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...TRANSPORT_PARAMS];
|
|
189
228
|
const deduped = new Map();
|
|
190
229
|
for (const param of params) {
|
|
191
230
|
const existing = deduped.get(param.paramName);
|
|
@@ -196,18 +235,75 @@ function collectParams(document, entry, operation, operationId) {
|
|
|
196
235
|
}
|
|
197
236
|
return {
|
|
198
237
|
params: [...deduped.values()],
|
|
199
|
-
paramsSchemaOptions:
|
|
200
|
-
preflightBlocks: [...operationParams.preflightBlocks, ...
|
|
201
|
-
requestFields: [...operationParams.requestFields, ...
|
|
202
|
-
sectionRenders: { ...operationParams.sectionRenders, ...
|
|
238
|
+
paramsSchemaOptions: qualifiedRequestBodyParams.paramsSchemaOptions,
|
|
239
|
+
preflightBlocks: [...operationParams.preflightBlocks, ...qualifiedRequestBodyParams.preflightBlocks],
|
|
240
|
+
requestFields: [...operationParams.requestFields, ...qualifiedRequestBodyParams.requestFields],
|
|
241
|
+
sectionRenders: { ...operationParams.sectionRenders, ...qualifiedRequestBodyParams.sectionRenders },
|
|
203
242
|
optionalSections: new Set([
|
|
204
243
|
...operationParams.optionalSections,
|
|
205
|
-
...
|
|
244
|
+
...qualifiedRequestBodyParams.optionalSections
|
|
206
245
|
]),
|
|
207
|
-
requestBodyDescription:
|
|
246
|
+
requestBodyDescription: qualifiedRequestBodyParams.requestBodyDescription,
|
|
247
|
+
bodyMode: qualifiedRequestBodyParams.bodyMode,
|
|
248
|
+
contentType: qualifiedRequestBodyParams.contentType,
|
|
249
|
+
multipartBinaryFields: qualifiedRequestBodyParams.multipartBinaryFields
|
|
208
250
|
};
|
|
209
251
|
}
|
|
210
|
-
function
|
|
252
|
+
function qualifyBodyParamCollisions(collected, reservedNames) {
|
|
253
|
+
const renames = new Map();
|
|
254
|
+
const usedNames = new Set(reservedNames);
|
|
255
|
+
for (const param of collected.params) {
|
|
256
|
+
let paramName = param.paramName;
|
|
257
|
+
if (usedNames.has(paramName)) {
|
|
258
|
+
const baseName = `body${toPascalCase(paramName)}`;
|
|
259
|
+
paramName = baseName;
|
|
260
|
+
let suffix = 2;
|
|
261
|
+
while (usedNames.has(paramName)) {
|
|
262
|
+
paramName = `${baseName}${suffix}`;
|
|
263
|
+
suffix += 1;
|
|
264
|
+
}
|
|
265
|
+
renames.set(param.paramName, paramName);
|
|
266
|
+
}
|
|
267
|
+
usedNames.add(paramName);
|
|
268
|
+
}
|
|
269
|
+
if (renames.size === 0) {
|
|
270
|
+
return collected;
|
|
271
|
+
}
|
|
272
|
+
const renameReference = (reference) => reference.kind === "param" && renames.has(reference.paramName)
|
|
273
|
+
? { ...reference, paramName: renames.get(reference.paramName) }
|
|
274
|
+
: reference;
|
|
275
|
+
return {
|
|
276
|
+
...collected,
|
|
277
|
+
params: collected.params.map((param) => ({
|
|
278
|
+
...param,
|
|
279
|
+
paramName: renames.get(param.paramName) ?? param.paramName
|
|
280
|
+
})),
|
|
281
|
+
preflightBlocks: collected.preflightBlocks.map((block) => block.kind === "scalar-null"
|
|
282
|
+
? {
|
|
283
|
+
...block,
|
|
284
|
+
paramName: renames.get(block.paramName) ?? block.paramName,
|
|
285
|
+
nullParamName: renames.get(block.nullParamName) ?? block.nullParamName
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
...block,
|
|
289
|
+
paramName: renames.get(block.paramName) ?? block.paramName,
|
|
290
|
+
jsonParamName: renames.get(block.jsonParamName) ?? block.jsonParamName,
|
|
291
|
+
...(block.nullParamName === undefined
|
|
292
|
+
? {}
|
|
293
|
+
: { nullParamName: renames.get(block.nullParamName) ?? block.nullParamName })
|
|
294
|
+
}),
|
|
295
|
+
requestFields: collected.requestFields.map((field) => ({
|
|
296
|
+
...field,
|
|
297
|
+
omitWhenUndefinedReference: renameReference(field.omitWhenUndefinedReference),
|
|
298
|
+
value: field.value.kind === "reference"
|
|
299
|
+
? { ...field.value, reference: renameReference(field.value.reference) }
|
|
300
|
+
: field.value.kind === "queryArray"
|
|
301
|
+
? { ...field.value, reference: renameReference(field.value.reference) }
|
|
302
|
+
: field.value
|
|
303
|
+
}))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function collectOperationParameters(document, path, pathItemParameters, operationParameters, operationId, auth) {
|
|
211
307
|
const merged = new Map();
|
|
212
308
|
for (const parameter of pathItemParameters) {
|
|
213
309
|
const resolved = expectParameter(document, parameter, operationId);
|
|
@@ -217,12 +313,28 @@ function collectOperationParameters(document, path, pathItemParameters, operatio
|
|
|
217
313
|
const resolved = expectParameter(document, parameter, operationId);
|
|
218
314
|
merged.set(`${resolved.in}:${resolved.name}`, resolved);
|
|
219
315
|
}
|
|
316
|
+
promoteQueryDeclaredPathParameters(path, merged);
|
|
220
317
|
assertPathTemplateParameters(path, merged, operationId);
|
|
221
318
|
const params = [];
|
|
222
319
|
const preflightBlocks = [];
|
|
223
320
|
const requestFields = [];
|
|
321
|
+
const usedParamNames = new Set();
|
|
224
322
|
for (const parameter of merged.values()) {
|
|
225
|
-
|
|
323
|
+
if (parameter.in === "header" &&
|
|
324
|
+
["accept", "content-type"].includes(parameter.name.toLowerCase())) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const paramName = usedParamNames.has(parameter.name)
|
|
328
|
+
? `${parameter.in}${toPascalCase(parameter.name)}`
|
|
329
|
+
: parameter.name;
|
|
330
|
+
const generated = createGeneratedParameter(document, paramName === parameter.name ? parameter : { ...parameter, name: paramName }, operationId, auth);
|
|
331
|
+
generated.requestField.wireName = generated.requestField.wireName.endsWith("[]")
|
|
332
|
+
? `${parameter.name}[]`
|
|
333
|
+
: parameter.name;
|
|
334
|
+
for (const param of generated.params) {
|
|
335
|
+
param.sourceName = parameter.name;
|
|
336
|
+
usedParamNames.add(param.paramName);
|
|
337
|
+
}
|
|
226
338
|
params.push(...generated.params);
|
|
227
339
|
preflightBlocks.push(...generated.preflightBlocks);
|
|
228
340
|
requestFields.push(generated.requestField);
|
|
@@ -232,11 +344,24 @@ function collectOperationParameters(document, path, pathItemParameters, operatio
|
|
|
232
344
|
paramsSchemaOptions: undefined,
|
|
233
345
|
preflightBlocks,
|
|
234
346
|
requestFields,
|
|
235
|
-
sectionRenders: { path: "wrapped", query: "wrapped" },
|
|
347
|
+
sectionRenders: { path: "wrapped", query: "wrapped", header: "wrapped" },
|
|
236
348
|
optionalSections: new Set(),
|
|
237
349
|
requestBodyDescription: undefined
|
|
238
350
|
};
|
|
239
351
|
}
|
|
352
|
+
function promoteQueryDeclaredPathParameters(path, parameters) {
|
|
353
|
+
for (const placeholder of collectPathPlaceholders(path)) {
|
|
354
|
+
if (parameters.has(`path:${placeholder}`)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const queryParameter = parameters.get(`query:${placeholder}`);
|
|
358
|
+
if (queryParameter === undefined) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
parameters.delete(`query:${placeholder}`);
|
|
362
|
+
parameters.set(`path:${placeholder}`, { ...queryParameter, in: "path", required: true });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
240
365
|
function collectRequestBodyParams(document, operation, operationId, method) {
|
|
241
366
|
if (operation.requestBody === undefined) {
|
|
242
367
|
return {
|
|
@@ -253,29 +378,65 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
253
378
|
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported requestBody on ${method.toUpperCase()}. Request bodies are not supported on GET in v1.`);
|
|
254
379
|
}
|
|
255
380
|
const requestBody = expectRequestBody(document, operation.requestBody, operationId, "requestBody");
|
|
256
|
-
const
|
|
381
|
+
const contentEntries = Object.entries(requestBody.content ?? {});
|
|
382
|
+
const contentEntry = contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isJsonMediaType(mediaType)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && mediaType.toLowerCase() === "application/x-www-form-urlencoded") ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isTextMediaType(mediaType)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isBinaryMediaType(mediaType)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && mediaType.toLowerCase() === "multipart/form-data");
|
|
383
|
+
const content = contentEntry?.[1];
|
|
384
|
+
const requestMediaType = contentEntry?.[0];
|
|
385
|
+
const bodyMode = requestMediaType?.toLowerCase() === "application/x-www-form-urlencoded"
|
|
386
|
+
? "form"
|
|
387
|
+
: requestMediaType?.toLowerCase() === "multipart/form-data"
|
|
388
|
+
? "multipart"
|
|
389
|
+
: requestMediaType !== undefined && !isJsonMediaType(requestMediaType)
|
|
390
|
+
? isTextMediaType(requestMediaType)
|
|
391
|
+
? "raw"
|
|
392
|
+
: "base64"
|
|
393
|
+
: "json";
|
|
257
394
|
if (content === undefined) {
|
|
258
395
|
throw new UserError(`Operation ${JSON.stringify(operationId)} must define a JSON request body media type in v1.`);
|
|
259
396
|
}
|
|
260
|
-
|
|
397
|
+
if (bodyMode === "raw" || bodyMode === "base64") {
|
|
398
|
+
const description = requestBody.description;
|
|
399
|
+
return {
|
|
400
|
+
...createCollectedRequestBodyParams([
|
|
401
|
+
createBodyField(document, "body", { type: "string", ...(description === undefined ? {} : { description }) }, requestBody.required !== true, operationId)
|
|
402
|
+
], requestBody.required !== true, description, "inline", undefined, bodyMode),
|
|
403
|
+
contentType: requestMediaType
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const schema = resolveBodySchema(document, content.schema, operationId, "requestBody");
|
|
261
407
|
const bodyOptional = requestBody.required !== true;
|
|
262
|
-
|
|
408
|
+
const multipartBinaryFields = bodyMode === "multipart" && schema.properties !== undefined
|
|
409
|
+
? Object.entries(schema.properties)
|
|
410
|
+
.filter(([, property]) => {
|
|
411
|
+
const resolved = resolveBodySchema(document, property, operationId, "requestBody multipart field");
|
|
412
|
+
return resolved.type === "string" && resolved.format === "binary";
|
|
413
|
+
})
|
|
414
|
+
.map(([name]) => name)
|
|
415
|
+
: undefined;
|
|
416
|
+
if (schema.type !== "object" || getCompositionKeyword(schema) !== undefined) {
|
|
263
417
|
const bodySchema = schema.description === undefined && requestBody.description !== undefined
|
|
264
418
|
? { ...schema, description: requestBody.description }
|
|
265
419
|
: schema;
|
|
266
|
-
return createCollectedRequestBodyParams([createBodyField(document, "body", bodySchema, bodyOptional, operationId)], bodyOptional, schema.description === undefined ? undefined : requestBody.description, "inline", undefined);
|
|
267
|
-
}
|
|
268
|
-
if (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
420
|
+
return createCollectedRequestBodyParams([createBodyField(document, "body", bodySchema, bodyOptional, operationId)], bodyOptional, schema.description === undefined ? undefined : requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
|
|
421
|
+
}
|
|
422
|
+
if ((schema.additionalProperties !== undefined && schema.additionalProperties !== false) ||
|
|
423
|
+
schema.properties === undefined) {
|
|
424
|
+
return createCollectedRequestBodyParams([createJsonBodyField({
|
|
425
|
+
document,
|
|
426
|
+
name: "body",
|
|
427
|
+
description: schema.description ?? requestBody.description,
|
|
428
|
+
schema,
|
|
429
|
+
optional: bodyOptional,
|
|
430
|
+
operationId,
|
|
431
|
+
context: "requestBody",
|
|
432
|
+
location: "body"
|
|
433
|
+
})], bodyOptional, requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
|
|
273
434
|
}
|
|
274
435
|
const required = new Set(schema.required ?? []);
|
|
275
436
|
const assemblies = [];
|
|
276
437
|
const declaredPropertyCount = Object.keys(schema.properties).length;
|
|
277
438
|
for (const [name, property] of Object.entries(schema.properties)) {
|
|
278
|
-
const propertySchema =
|
|
439
|
+
const propertySchema = resolveBodySchema(document, property, operationId, `requestBody.properties.${name}`);
|
|
279
440
|
if (propertySchema.readOnly === true) {
|
|
280
441
|
continue;
|
|
281
442
|
}
|
|
@@ -283,14 +444,31 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
283
444
|
assemblies.push(generated);
|
|
284
445
|
}
|
|
285
446
|
if (!bodyOptional && assemblies.length === 0) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
447
|
+
if (declaredPropertyCount === 0) {
|
|
448
|
+
return {
|
|
449
|
+
params: [],
|
|
450
|
+
paramsSchemaOptions: schema.additionalProperties === false ? { additionalProperties: false } : undefined,
|
|
451
|
+
preflightBlocks: [],
|
|
452
|
+
requestFields: [{
|
|
453
|
+
location: "body",
|
|
454
|
+
wireName: "body",
|
|
455
|
+
value: { kind: "emptyObject" },
|
|
456
|
+
omitWhenUndefinedReference: { kind: "resolved", resolvedName: "emptyBody" }
|
|
457
|
+
}],
|
|
458
|
+
sectionRenders: { body: "inline" },
|
|
459
|
+
optionalSections: new Set(),
|
|
460
|
+
requestBodyDescription: requestBody.description,
|
|
461
|
+
bodyMode,
|
|
462
|
+
...(multipartBinaryFields === undefined ? {} : { multipartBinaryFields })
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} requestBody is required but all declared fields are readOnly.`);
|
|
290
466
|
}
|
|
291
|
-
return createCollectedRequestBodyParams(assemblies, bodyOptional, requestBody.description, "wrapped", schema.additionalProperties === false ? { additionalProperties: false } : undefined);
|
|
467
|
+
return createCollectedRequestBodyParams(assemblies, bodyOptional, requestBody.description, "wrapped", schema.additionalProperties === false ? { additionalProperties: false } : undefined, bodyMode, multipartBinaryFields);
|
|
292
468
|
}
|
|
293
|
-
function
|
|
469
|
+
function resolveSuccessResponse(document, operation, operationId) {
|
|
470
|
+
let textualMediaType;
|
|
471
|
+
let binaryMediaType;
|
|
294
472
|
for (const [statusCode, response] of Object.entries(operation.responses ?? {})) {
|
|
295
473
|
if (!isSuccessStatusCode(statusCode)) {
|
|
296
474
|
continue;
|
|
@@ -300,45 +478,33 @@ function assertSupportedSuccessResponses(document, operation, operationId) {
|
|
|
300
478
|
.filter(([, mediaType]) => mediaType !== undefined)
|
|
301
479
|
.map(([mediaType]) => mediaType);
|
|
302
480
|
if (declaredMediaTypes.length === 0 ||
|
|
303
|
-
declaredMediaTypes.
|
|
304
|
-
for (const [mediaType, mediaTypeObject] of Object.entries(resolvedResponse.content ?? {})) {
|
|
305
|
-
if (mediaTypeObject === undefined ||
|
|
306
|
-
mediaTypeObject.schema === undefined ||
|
|
307
|
-
!isJsonMediaType(mediaType)) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
assertSupportedSuccessResponseSchema(document, mediaTypeObject.schema, operationId, `success response schema for status ${JSON.stringify(statusCode)}`);
|
|
311
|
-
}
|
|
481
|
+
declaredMediaTypes.some((mediaType) => isJsonMediaType(mediaType))) {
|
|
312
482
|
continue;
|
|
313
483
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const resolvedSchema = expectSupportedSuccessResponseSchema(document, schema, operationId, context);
|
|
321
|
-
if (resolvedSchema.additionalProperties !== undefined &&
|
|
322
|
-
resolvedSchema.additionalProperties !== false) {
|
|
323
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Object response schemas with additionalProperties are not supported in v1.`);
|
|
324
|
-
}
|
|
325
|
-
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema.properties ?? {})) {
|
|
326
|
-
assertSupportedSuccessResponseSchema(document, propertySchema, operationId, `${context} property ${JSON.stringify(propertyName)}`);
|
|
484
|
+
const textual = declaredMediaTypes.find(isTextMediaType);
|
|
485
|
+
if (textual !== undefined && declaredMediaTypes.every(isTextMediaType)) {
|
|
486
|
+
textualMediaType ??= textual;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
binaryMediaType ??= declaredMediaTypes[0];
|
|
327
490
|
}
|
|
328
|
-
if (
|
|
329
|
-
|
|
491
|
+
if (binaryMediaType !== undefined) {
|
|
492
|
+
return { mode: "binary", accept: binaryMediaType };
|
|
330
493
|
}
|
|
494
|
+
return textualMediaType === undefined
|
|
495
|
+
? { mode: "json", accept: "application/json" }
|
|
496
|
+
: { mode: "text", accept: textualMediaType };
|
|
331
497
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
498
|
+
function createGeneratedParameter(document, parameter, operationId, auth) {
|
|
499
|
+
if (parameter.in === "header" &&
|
|
500
|
+
parameter.name.toLowerCase() === "authorization" &&
|
|
501
|
+
auth === "required") {
|
|
502
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} header parameter ${JSON.stringify(parameter.name)} is managed by the HTTP transport and cannot be generated as a command parameter.`);
|
|
337
503
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
504
|
+
const context = `parameter ${JSON.stringify(parameter.name)}`;
|
|
505
|
+
const schema = parameter.in === "query" && parameter.style === "deepObject"
|
|
506
|
+
? resolveBodySchema(document, parameter.schema, operationId, context)
|
|
507
|
+
: expectSchema(document, parameter.schema, operationId, context);
|
|
342
508
|
if (parameter.in === "path") {
|
|
343
509
|
if (parameter.required !== true) {
|
|
344
510
|
throw new UserError(`Operation ${JSON.stringify(operationId)} path parameter ${JSON.stringify(parameter.name)} must set required: true.`);
|
|
@@ -348,19 +514,43 @@ function createGeneratedParameter(document, parameter, operationId) {
|
|
|
348
514
|
}
|
|
349
515
|
assertSupportedPathParameterSerialization(parameter, operationId);
|
|
350
516
|
}
|
|
351
|
-
|
|
517
|
+
if (parameter.in === "query" &&
|
|
518
|
+
parameter.style === "deepObject" &&
|
|
519
|
+
(getCompositionKeyword(schema) !== undefined ||
|
|
520
|
+
(schema.type === "array" && schema.items !== undefined && isComplexJsonBodySchema(document, schema.items, operationId, `${context} items`)))) {
|
|
521
|
+
return createJsonQueryField({
|
|
522
|
+
document,
|
|
523
|
+
name: parameter.name,
|
|
524
|
+
description: parameter.description ?? schema.description,
|
|
525
|
+
schema,
|
|
526
|
+
optional: parameter.required !== true,
|
|
527
|
+
operationId,
|
|
528
|
+
context,
|
|
529
|
+
location: "query",
|
|
530
|
+
queryObjectSerialization: resolveQueryObjectSerialization(parameter, operationId)
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const querySerialization = parameter.in === "query" && schema.type === "array"
|
|
534
|
+
? resolveQueryArraySerialization(parameter, operationId)
|
|
535
|
+
: undefined;
|
|
536
|
+
const generated = createField({
|
|
352
537
|
document,
|
|
353
538
|
name: parameter.name,
|
|
354
539
|
description: parameter.description ?? schema.description,
|
|
355
540
|
schema,
|
|
356
541
|
optional: parameter.required !== true,
|
|
357
542
|
operationId,
|
|
358
|
-
context
|
|
543
|
+
context,
|
|
359
544
|
location: parameter.in,
|
|
360
|
-
querySerialization
|
|
361
|
-
|
|
545
|
+
querySerialization,
|
|
546
|
+
queryObjectSerialization: parameter.in === "query" && schema.type === "object"
|
|
547
|
+
? resolveQueryObjectSerialization(parameter, operationId)
|
|
362
548
|
: undefined
|
|
363
549
|
});
|
|
550
|
+
if (querySerialization === "brackets") {
|
|
551
|
+
generated.requestField.wireName = `${parameter.name}[]`;
|
|
552
|
+
}
|
|
553
|
+
return generated;
|
|
364
554
|
}
|
|
365
555
|
function assertSupportedPathParameterSerialization(parameter, operationId) {
|
|
366
556
|
const style = parameter.style ?? "simple";
|
|
@@ -371,6 +561,18 @@ function assertSupportedPathParameterSerialization(parameter, operationId) {
|
|
|
371
561
|
throw new UserError(`Operation ${JSON.stringify(operationId)} path parameter ${JSON.stringify(parameter.name)} uses unsupported serialization. Path parameters must use style "simple" with explode false in v1.`);
|
|
372
562
|
}
|
|
373
563
|
function createBodyField(document, name, schema, optional, operationId) {
|
|
564
|
+
if (isComplexJsonBodySchema(document, schema, operationId, `request body field ${JSON.stringify(name)}`)) {
|
|
565
|
+
return createJsonBodyField({
|
|
566
|
+
document,
|
|
567
|
+
name,
|
|
568
|
+
description: schema.description,
|
|
569
|
+
schema,
|
|
570
|
+
optional,
|
|
571
|
+
operationId,
|
|
572
|
+
context: `request body field ${JSON.stringify(name)}`,
|
|
573
|
+
location: "body"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
374
576
|
return createField({
|
|
375
577
|
document,
|
|
376
578
|
name,
|
|
@@ -382,6 +584,32 @@ function createBodyField(document, name, schema, optional, operationId) {
|
|
|
382
584
|
location: "body"
|
|
383
585
|
});
|
|
384
586
|
}
|
|
587
|
+
function isComplexJsonBodySchema(document, schema, operationId, context) {
|
|
588
|
+
if (isUnconstrainedJsonSchema(schema)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
if (schema.type === "object" ||
|
|
592
|
+
schema.properties !== undefined ||
|
|
593
|
+
schema.additionalProperties !== undefined ||
|
|
594
|
+
getCompositionKeyword(schema) !== undefined ||
|
|
595
|
+
Array.isArray(schema.type)) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
if (schema.type !== "array" || schema.items === undefined) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const itemSchema = resolveBodySchema(document, schema.items, operationId, `${context} items`);
|
|
602
|
+
return isComplexJsonBodySchema(document, itemSchema, operationId, `${context} items`);
|
|
603
|
+
}
|
|
604
|
+
function isUnconstrainedJsonSchema(schema) {
|
|
605
|
+
return (schema.type === undefined &&
|
|
606
|
+
schema.enum === undefined &&
|
|
607
|
+
schema.properties === undefined &&
|
|
608
|
+
schema.additionalProperties === undefined &&
|
|
609
|
+
schema.allOf === undefined &&
|
|
610
|
+
schema.anyOf === undefined &&
|
|
611
|
+
schema.oneOf === undefined);
|
|
612
|
+
}
|
|
385
613
|
function createField(options) {
|
|
386
614
|
return FIELD_ASSEMBLERS[options.location][getFieldSchemaKind(options.schema)](options);
|
|
387
615
|
}
|
|
@@ -442,11 +670,53 @@ function createScalarParam(options) {
|
|
|
442
670
|
function getFieldSchemaKind(schema) {
|
|
443
671
|
return schema.type === "array" ? "array" : schema.type === "object" ? "object" : "scalar";
|
|
444
672
|
}
|
|
445
|
-
function
|
|
446
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)}
|
|
673
|
+
function createScalarOnlyParameterError(name, operationId, location) {
|
|
674
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} ${location} parameter ${JSON.stringify(name)} must use a scalar schema (string, number, integer, or boolean).`);
|
|
447
675
|
}
|
|
448
|
-
function
|
|
449
|
-
|
|
676
|
+
function createJsonBodyField(options) {
|
|
677
|
+
return {
|
|
678
|
+
params: [
|
|
679
|
+
{
|
|
680
|
+
paramName: options.name,
|
|
681
|
+
sourceName: options.name,
|
|
682
|
+
location: "body",
|
|
683
|
+
description: options.description,
|
|
684
|
+
optional: options.optional,
|
|
685
|
+
definition: { kind: "json" }
|
|
686
|
+
}
|
|
687
|
+
],
|
|
688
|
+
preflightBlocks: [],
|
|
689
|
+
requestField: {
|
|
690
|
+
location: "body",
|
|
691
|
+
wireName: options.name,
|
|
692
|
+
value: { kind: "reference", reference: { kind: "param", paramName: options.name } },
|
|
693
|
+
omitWhenUndefinedReference: { kind: "param", paramName: options.name }
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function createJsonQueryField(options) {
|
|
698
|
+
if (options.queryObjectSerialization !== "deepObject") {
|
|
699
|
+
throw new ToolcraftBugError("Missing deep-object serialization for generated JSON query field.");
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
params: [
|
|
703
|
+
{
|
|
704
|
+
paramName: options.name,
|
|
705
|
+
sourceName: options.name,
|
|
706
|
+
location: "query",
|
|
707
|
+
description: options.description,
|
|
708
|
+
optional: options.optional,
|
|
709
|
+
definition: { kind: "json" }
|
|
710
|
+
}
|
|
711
|
+
],
|
|
712
|
+
preflightBlocks: [],
|
|
713
|
+
requestField: {
|
|
714
|
+
location: "query",
|
|
715
|
+
wireName: options.name,
|
|
716
|
+
value: { kind: "reference", reference: { kind: "param", paramName: options.name } },
|
|
717
|
+
omitWhenUndefinedReference: { kind: "param", paramName: options.name }
|
|
718
|
+
}
|
|
719
|
+
};
|
|
450
720
|
}
|
|
451
721
|
function createArrayParam(options) {
|
|
452
722
|
const { document, name, description, schema, optional, operationId, context, location } = options;
|
|
@@ -526,15 +796,23 @@ const FIELD_ASSEMBLERS = {
|
|
|
526
796
|
...options,
|
|
527
797
|
location: "body"
|
|
528
798
|
}),
|
|
529
|
-
object: (options) =>
|
|
799
|
+
object: (options) => createJsonBodyField(options),
|
|
530
800
|
scalar: (options) => createScalarParam({
|
|
531
801
|
...options,
|
|
532
802
|
location: "body"
|
|
533
803
|
})
|
|
534
804
|
},
|
|
805
|
+
header: {
|
|
806
|
+
array: (options) => createScalarOnlyParameterError(options.name, options.operationId, "header"),
|
|
807
|
+
object: (options) => createScalarOnlyParameterError(options.name, options.operationId, "header"),
|
|
808
|
+
scalar: (options) => createScalarParam({
|
|
809
|
+
...options,
|
|
810
|
+
location: "header"
|
|
811
|
+
})
|
|
812
|
+
},
|
|
535
813
|
path: {
|
|
536
|
-
array: (options) =>
|
|
537
|
-
object: (options) =>
|
|
814
|
+
array: (options) => createPathArrayParam(options),
|
|
815
|
+
object: (options) => createScalarOnlyParameterError(options.name, options.operationId, "path"),
|
|
538
816
|
scalar: (options) => createScalarParam({
|
|
539
817
|
...options,
|
|
540
818
|
location: "path"
|
|
@@ -545,16 +823,34 @@ const FIELD_ASSEMBLERS = {
|
|
|
545
823
|
...options,
|
|
546
824
|
location: "query"
|
|
547
825
|
}),
|
|
548
|
-
object: (options) =>
|
|
549
|
-
...options,
|
|
550
|
-
location: "query"
|
|
551
|
-
}),
|
|
826
|
+
object: (options) => createJsonQueryField(options),
|
|
552
827
|
scalar: (options) => createScalarParam({
|
|
553
828
|
...options,
|
|
554
829
|
location: "query"
|
|
555
830
|
})
|
|
556
831
|
}
|
|
557
832
|
};
|
|
833
|
+
function createPathArrayParam(options) {
|
|
834
|
+
const definition = createParamDefinition(options.document, options.schema, options.operationId, options.context);
|
|
835
|
+
const paramName = options.name;
|
|
836
|
+
return {
|
|
837
|
+
params: [{
|
|
838
|
+
paramName,
|
|
839
|
+
sourceName: options.name,
|
|
840
|
+
location: "path",
|
|
841
|
+
description: options.description,
|
|
842
|
+
optional: false,
|
|
843
|
+
definition
|
|
844
|
+
}],
|
|
845
|
+
preflightBlocks: [],
|
|
846
|
+
requestField: {
|
|
847
|
+
location: "path",
|
|
848
|
+
wireName: options.name,
|
|
849
|
+
value: { kind: "queryArray", reference: { kind: "param", paramName }, serialization: "comma" },
|
|
850
|
+
omitWhenUndefinedReference: { kind: "param", paramName }
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
558
854
|
function expectQueryArraySerialization(querySerialization) {
|
|
559
855
|
if (querySerialization === undefined) {
|
|
560
856
|
throw new ToolcraftBugError("Missing query array serialization for generated query array field.");
|
|
@@ -564,7 +860,7 @@ function expectQueryArraySerialization(querySerialization) {
|
|
|
564
860
|
function supportsNullHelper(location, shape, nullable) {
|
|
565
861
|
return nullable === true && NULL_HELPER_SUPPORT[location][shape];
|
|
566
862
|
}
|
|
567
|
-
function createCollectedRequestBodyParams(assemblies, bodyOptional, requestBodyDescription, bodyRender, paramsSchemaOptions) {
|
|
863
|
+
function createCollectedRequestBodyParams(assemblies, bodyOptional, requestBodyDescription, bodyRender, paramsSchemaOptions, bodyMode, multipartBinaryFields) {
|
|
568
864
|
return {
|
|
569
865
|
params: assemblies.flatMap((assembly) => assembly.params),
|
|
570
866
|
paramsSchemaOptions,
|
|
@@ -574,7 +870,9 @@ function createCollectedRequestBodyParams(assemblies, bodyOptional, requestBodyD
|
|
|
574
870
|
optionalSections: bodyOptional
|
|
575
871
|
? new Set(["body"])
|
|
576
872
|
: new Set(),
|
|
577
|
-
requestBodyDescription
|
|
873
|
+
requestBodyDescription,
|
|
874
|
+
bodyMode,
|
|
875
|
+
...(multipartBinaryFields === undefined ? {} : { multipartBinaryFields })
|
|
578
876
|
};
|
|
579
877
|
}
|
|
580
878
|
function createParamDefinition(document, schema, operationId, context) {
|
|
@@ -595,7 +893,7 @@ function createParamDefinition(document, schema, operationId, context) {
|
|
|
595
893
|
const scalarDefinition = isOpenApiScalarType(schema.type)
|
|
596
894
|
? SCHEMA_TYPE_TO_KIND[schema.type]
|
|
597
895
|
: undefined;
|
|
598
|
-
const enumValues = normalizeEnumValues(schema.enum, operationId, context
|
|
896
|
+
const enumValues = normalizeEnumValues(schema.enum, operationId, context);
|
|
599
897
|
if (enumValues !== undefined) {
|
|
600
898
|
return {
|
|
601
899
|
kind: "enum",
|
|
@@ -670,44 +968,32 @@ function isAsciiDigit(character) {
|
|
|
670
968
|
}
|
|
671
969
|
function isJsonMediaType(mediaType) {
|
|
672
970
|
const normalized = mediaType.toLowerCase();
|
|
673
|
-
return normalized.includes("application/json") || normalized.includes("+json");
|
|
971
|
+
return normalized === "*/*" || normalized === "text/json" || normalized.includes("application/json") || normalized.includes("+json");
|
|
674
972
|
}
|
|
675
|
-
function
|
|
973
|
+
function isTextMediaType(mediaType) {
|
|
974
|
+
const normalized = mediaType.toLowerCase();
|
|
975
|
+
return normalized.startsWith("text/") || normalized === "plain/text" || normalized === "application/jwt";
|
|
976
|
+
}
|
|
977
|
+
function isBinaryMediaType(mediaType) {
|
|
978
|
+
const normalized = mediaType.toLowerCase();
|
|
979
|
+
return (normalized === "application/octet-stream" ||
|
|
980
|
+
normalized === "application/zip" ||
|
|
981
|
+
normalized === "application/gzip" ||
|
|
982
|
+
normalized === "application/pdf");
|
|
983
|
+
}
|
|
984
|
+
function normalizeEnumValues(enumValues, operationId, context) {
|
|
676
985
|
if (enumValues === undefined) {
|
|
677
986
|
return undefined;
|
|
678
987
|
}
|
|
679
|
-
const filteredValues = enumValues.filter(
|
|
680
|
-
if (enumValues.includes(null) && nullable !== true) {
|
|
681
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Enums may include null only when the schema is marked nullable.`);
|
|
682
|
-
}
|
|
988
|
+
const filteredValues = enumValues.filter(isEnumPrimitiveValue);
|
|
683
989
|
if (filteredValues.length === 0) {
|
|
684
990
|
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Enum values cannot be empty.`);
|
|
685
991
|
}
|
|
686
|
-
if (filteredValues.some((value) => !isEnumPrimitiveValue(value))) {
|
|
687
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. OpenAPI enums must contain only string, number, boolean, or null values.`);
|
|
688
|
-
}
|
|
689
992
|
const primitiveTypes = new Set(filteredValues.map((value) => typeof value));
|
|
690
993
|
if (primitiveTypes.size > 1) {
|
|
691
994
|
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Enums must not mix primitive types.`);
|
|
692
995
|
}
|
|
693
|
-
|
|
694
|
-
const matchesSchemaType = filteredValues.every((value) => {
|
|
695
|
-
if (schemaType === "integer") {
|
|
696
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
697
|
-
}
|
|
698
|
-
if (schemaType === "number") {
|
|
699
|
-
return typeof value === "number";
|
|
700
|
-
}
|
|
701
|
-
if (schemaType === "string" || schemaType === "boolean") {
|
|
702
|
-
return typeof value === schemaType;
|
|
703
|
-
}
|
|
704
|
-
return true;
|
|
705
|
-
});
|
|
706
|
-
if (!matchesSchemaType) {
|
|
707
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Enum values must match declared schema.type ${JSON.stringify(schemaType)}.`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return filteredValues.filter(isEnumPrimitiveValue);
|
|
996
|
+
return filteredValues.filter((value) => value !== null);
|
|
711
997
|
}
|
|
712
998
|
function resolveLocalReference(document, ref, operationId, context) {
|
|
713
999
|
if (!ref.startsWith("#/")) {
|
|
@@ -715,7 +1001,9 @@ function resolveLocalReference(document, ref, operationId, context) {
|
|
|
715
1001
|
}
|
|
716
1002
|
let current = document;
|
|
717
1003
|
for (const segment of ref.slice(2).split("/").map(unescapeJsonPointerSegment)) {
|
|
718
|
-
if (typeof current !== "object" ||
|
|
1004
|
+
if (typeof current !== "object" ||
|
|
1005
|
+
current === null ||
|
|
1006
|
+
!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
719
1007
|
throw new UserError(`Operation ${JSON.stringify(operationId)} references missing $ref ${JSON.stringify(ref)} in ${context}.`);
|
|
720
1008
|
}
|
|
721
1009
|
current = current[segment];
|
|
@@ -723,7 +1011,13 @@ function resolveLocalReference(document, ref, operationId, context) {
|
|
|
723
1011
|
return current;
|
|
724
1012
|
}
|
|
725
1013
|
function unescapeJsonPointerSegment(segment) {
|
|
726
|
-
|
|
1014
|
+
const unescaped = segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
1015
|
+
try {
|
|
1016
|
+
return decodeURIComponent(unescaped);
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
return unescaped;
|
|
1020
|
+
}
|
|
727
1021
|
}
|
|
728
1022
|
function expectParameter(document, parameter, operationId, refChain = []) {
|
|
729
1023
|
if (isReferenceObject(parameter)) {
|
|
@@ -733,8 +1027,8 @@ function expectParameter(document, parameter, operationId, refChain = []) {
|
|
|
733
1027
|
if (parameter.content !== undefined) {
|
|
734
1028
|
throw new UserError(`Operation ${JSON.stringify(operationId)} parameter ${JSON.stringify(parameter.name)} uses unsupported parameter.content. Define path/query parameters with parameter.schema in v1.`);
|
|
735
1029
|
}
|
|
736
|
-
if (parameter.in !== "path" && parameter.in !== "query") {
|
|
737
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported parameter location ${JSON.stringify(parameter.in)}. Only path and
|
|
1030
|
+
if (parameter.in !== "path" && parameter.in !== "query" && parameter.in !== "header") {
|
|
1031
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported parameter location ${JSON.stringify(parameter.in)}. Only path, query, and header parameters are supported in v1; use auth or handwritten commands for cookies.`);
|
|
738
1032
|
}
|
|
739
1033
|
return parameter;
|
|
740
1034
|
}
|
|
@@ -751,7 +1045,7 @@ function isEnumPrimitiveValue(value) {
|
|
|
751
1045
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
752
1046
|
}
|
|
753
1047
|
function isOpenApiScalarType(type) {
|
|
754
|
-
return typeof type === "string" && Object.prototype.hasOwnProperty.call(SCHEMA_TYPE_TO_KIND, type);
|
|
1048
|
+
return (typeof type === "string" && Object.prototype.hasOwnProperty.call(SCHEMA_TYPE_TO_KIND, type));
|
|
755
1049
|
}
|
|
756
1050
|
function expectRequestBody(document, requestBody, operationId, context, refChain = []) {
|
|
757
1051
|
if (!isReferenceObject(requestBody)) {
|
|
@@ -776,24 +1070,86 @@ function expectSchema(document, schema, operationId, context, refChain = []) {
|
|
|
776
1070
|
}
|
|
777
1071
|
return resolvedSchema;
|
|
778
1072
|
}
|
|
1073
|
+
function resolveBodySchema(document, schema, operationId, context) {
|
|
1074
|
+
return normalizeNullableSchema(document, resolveSchema(document, schema, operationId, context), operationId, context);
|
|
1075
|
+
}
|
|
779
1076
|
function resolveSchema(document, schema, operationId, context, refChain = []) {
|
|
780
1077
|
if (schema === undefined) {
|
|
781
1078
|
throw new UserError(`Operation ${JSON.stringify(operationId)} is missing a schema for ${context}.`);
|
|
782
1079
|
}
|
|
783
1080
|
if (isReferenceObject(schema)) {
|
|
784
1081
|
assertAcyclicRef(schema.$ref, refChain, operationId, context);
|
|
785
|
-
|
|
1082
|
+
const resolved = resolveLocalReference(document, schema.$ref, operationId, context);
|
|
1083
|
+
const nestedSchema = isSchemaContainer(resolved) ? resolved.schema : resolved;
|
|
1084
|
+
return resolveSchema(document, nestedSchema, operationId, context, [...refChain, schema.$ref]);
|
|
1085
|
+
}
|
|
1086
|
+
return cloneOwnSchemaObject(schema);
|
|
1087
|
+
}
|
|
1088
|
+
function cloneOwnSchemaObject(schema) {
|
|
1089
|
+
const clone = Object.create(null);
|
|
1090
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1091
|
+
Object.defineProperty(clone, key, {
|
|
1092
|
+
value,
|
|
1093
|
+
enumerable: true,
|
|
1094
|
+
configurable: true,
|
|
1095
|
+
writable: true
|
|
1096
|
+
});
|
|
786
1097
|
}
|
|
787
|
-
return
|
|
1098
|
+
return clone;
|
|
1099
|
+
}
|
|
1100
|
+
function isSchemaContainer(value) {
|
|
1101
|
+
return !isReferenceObject(value) && "schema" in value && value.schema !== undefined;
|
|
788
1102
|
}
|
|
789
1103
|
function normalizeNullableSchema(document, schema, operationId, context) {
|
|
790
1104
|
const typeNormalizedSchema = normalizeNullableTypeArray(schema);
|
|
1105
|
+
const equivalentComposition = resolveEquivalentCompositionSchema(document, typeNormalizedSchema, operationId, context);
|
|
1106
|
+
if (equivalentComposition !== undefined) {
|
|
1107
|
+
return equivalentComposition;
|
|
1108
|
+
}
|
|
791
1109
|
if (getCompositionKeyword(typeNormalizedSchema) !== "anyOf") {
|
|
792
1110
|
return typeNormalizedSchema;
|
|
793
1111
|
}
|
|
794
1112
|
return (resolveNullableAnyOfSchema(document, typeNormalizedSchema, operationId, context) ??
|
|
795
1113
|
typeNormalizedSchema);
|
|
796
1114
|
}
|
|
1115
|
+
function resolveEquivalentCompositionSchema(document, schema, operationId, context) {
|
|
1116
|
+
const keyword = getCompositionKeyword(schema);
|
|
1117
|
+
if (keyword !== "anyOf" && keyword !== "oneOf") {
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
const variants = schema[keyword];
|
|
1121
|
+
if (variants === undefined || variants.length === 0) {
|
|
1122
|
+
return undefined;
|
|
1123
|
+
}
|
|
1124
|
+
const resolved = variants.map((variant, index) => normalizeNullableSchema(document, resolveSchema(document, variant, operationId, `${context} ${keyword} variant ${index}`), operationId, `${context} ${keyword} variant ${index}`));
|
|
1125
|
+
const shapes = resolved.map(getEquivalentSchemaShape);
|
|
1126
|
+
const [firstShape] = shapes;
|
|
1127
|
+
if (firstShape === undefined || shapes.some((shape) => shape !== firstShape)) {
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
const { anyOf: _anyOf, oneOf: _oneOf, ...wrapper } = schema;
|
|
1131
|
+
void _anyOf;
|
|
1132
|
+
void _oneOf;
|
|
1133
|
+
return { ...wrapper, ...createSchemaFromEquivalentShape(firstShape) };
|
|
1134
|
+
}
|
|
1135
|
+
function getEquivalentSchemaShape(schema) {
|
|
1136
|
+
if (getCompositionKeyword(schema) !== undefined || Array.isArray(schema.type)) {
|
|
1137
|
+
return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
if (schema.type === "array") {
|
|
1140
|
+
if (schema.items === undefined || isReferenceObject(schema.items))
|
|
1141
|
+
return undefined;
|
|
1142
|
+
const itemShape = getEquivalentSchemaShape(schema.items);
|
|
1143
|
+
return itemShape === undefined ? undefined : `array:${itemShape}`;
|
|
1144
|
+
}
|
|
1145
|
+
return isOpenApiScalarType(schema.type) ? schema.type : undefined;
|
|
1146
|
+
}
|
|
1147
|
+
function createSchemaFromEquivalentShape(shape) {
|
|
1148
|
+
if (!shape.startsWith("array:")) {
|
|
1149
|
+
return { type: shape };
|
|
1150
|
+
}
|
|
1151
|
+
return { type: "array", items: createSchemaFromEquivalentShape(shape.slice("array:".length)) };
|
|
1152
|
+
}
|
|
797
1153
|
function normalizeNullableTypeArray(schema) {
|
|
798
1154
|
if (!Array.isArray(schema.type)) {
|
|
799
1155
|
return schema;
|
|
@@ -810,7 +1166,8 @@ function normalizeNullableTypeArray(schema) {
|
|
|
810
1166
|
}
|
|
811
1167
|
function getCompositionKeyword(schema) {
|
|
812
1168
|
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
813
|
-
if (schema
|
|
1169
|
+
if (Object.prototype.hasOwnProperty.call(schema, keyword) &&
|
|
1170
|
+
schema[keyword] !== undefined) {
|
|
814
1171
|
return keyword;
|
|
815
1172
|
}
|
|
816
1173
|
}
|
|
@@ -872,6 +1229,59 @@ function assertUniqueCommandPaths(commands) {
|
|
|
872
1229
|
seen.set(key, command);
|
|
873
1230
|
}
|
|
874
1231
|
}
|
|
1232
|
+
function disambiguateCommandPaths(commands) {
|
|
1233
|
+
const byCommandPath = new Map();
|
|
1234
|
+
for (const command of commands) {
|
|
1235
|
+
const key = `${command.noun} ${command.verb}`;
|
|
1236
|
+
byCommandPath.set(key, [...(byCommandPath.get(key) ?? []), command]);
|
|
1237
|
+
}
|
|
1238
|
+
for (const collisions of byCommandPath.values()) {
|
|
1239
|
+
if (collisions.length < 2) {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
const existingPaths = new Set(commands
|
|
1243
|
+
.filter((command) => !collisions.includes(command))
|
|
1244
|
+
.map((command) => `${command.noun} ${command.verb}`));
|
|
1245
|
+
const operationIdCandidates = collisions.map((command) => ({
|
|
1246
|
+
command,
|
|
1247
|
+
verb: deriveDisambiguatedVerb(command.operationId, command.noun)
|
|
1248
|
+
}));
|
|
1249
|
+
if (applyDisambiguatedVerbs(operationIdCandidates, existingPaths)) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const pathCandidates = collisions.map((command) => ({
|
|
1253
|
+
command,
|
|
1254
|
+
verb: derivePathDisambiguatedVerb(command.method.toLowerCase(), command.path, command.noun, command.verb)
|
|
1255
|
+
}));
|
|
1256
|
+
if (applyDisambiguatedVerbs(pathCandidates, existingPaths)) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (applyDisambiguatedVerbs(collisions.map((command) => ({
|
|
1260
|
+
command,
|
|
1261
|
+
verb: derivePathDisambiguatedVerb(command.method.toLowerCase(), command.path, command.noun, command.verb, true)
|
|
1262
|
+
})), existingPaths)) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
applyDisambiguatedVerbs(collisions.map((command) => ({
|
|
1266
|
+
command,
|
|
1267
|
+
verb: derivePathDisambiguatedVerb(command.method.toLowerCase(), command.path, command.noun, command.verb, true, true)
|
|
1268
|
+
})), existingPaths);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function applyDisambiguatedVerbs(candidates, existingPaths) {
|
|
1272
|
+
const candidatePaths = candidates.map(({ command, verb }) => `${command.noun} ${verb}`);
|
|
1273
|
+
if (candidates.some(({ verb }) => verb.length === 0) ||
|
|
1274
|
+
new Set(candidatePaths).size !== candidatePaths.length ||
|
|
1275
|
+
candidatePaths.some((path) => existingPaths.has(path))) {
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
for (const { command, verb } of candidates) {
|
|
1279
|
+
command.verb = verb;
|
|
1280
|
+
command.exportName = `${toCamelCase(command.noun)}${toPascalCase(verb)}Command`;
|
|
1281
|
+
command.filePath = `${command.noun}/${verb}.ts`;
|
|
1282
|
+
}
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
875
1285
|
function createCommandFile(options) {
|
|
876
1286
|
const requiresUserError = options.preflightBlocks.length > 0;
|
|
877
1287
|
const lines = createGeneratedTypeScriptFileLines([
|
|
@@ -896,10 +1306,36 @@ function createCommandFile(options) {
|
|
|
896
1306
|
lines.push(" handler: async ({ params, baseUrl, tokenSource, fetch }) => {");
|
|
897
1307
|
lines.push(...options.preflightBlocks.flatMap((block) => renderPreflightBlock(block)));
|
|
898
1308
|
lines.push(" return requestJson({");
|
|
899
|
-
lines.push(
|
|
1309
|
+
lines.push(options.baseUrl === undefined
|
|
1310
|
+
? " baseUrl,"
|
|
1311
|
+
: ` baseUrl: ${JSON.stringify(options.baseUrl)},`);
|
|
900
1312
|
lines.push(` path: ${JSON.stringify(options.path)},`);
|
|
901
1313
|
lines.push(` method: ${JSON.stringify(options.method)},`);
|
|
902
1314
|
lines.push(` auth: ${JSON.stringify(options.auth)},`);
|
|
1315
|
+
if (options.responseMode !== "json") {
|
|
1316
|
+
lines.push(` responseMode: ${JSON.stringify(options.responseMode)},`);
|
|
1317
|
+
}
|
|
1318
|
+
if (options.accept !== "application/json") {
|
|
1319
|
+
lines.push(` accept: ${JSON.stringify(options.accept)},`);
|
|
1320
|
+
}
|
|
1321
|
+
if (options.bodyMode === "form") {
|
|
1322
|
+
lines.push(' bodyMode: "form",');
|
|
1323
|
+
}
|
|
1324
|
+
else if (options.bodyMode === "raw") {
|
|
1325
|
+
lines.push(' bodyMode: "raw",');
|
|
1326
|
+
}
|
|
1327
|
+
else if (options.bodyMode === "base64") {
|
|
1328
|
+
lines.push(' bodyMode: "base64",');
|
|
1329
|
+
}
|
|
1330
|
+
else if (options.bodyMode === "multipart") {
|
|
1331
|
+
lines.push(' bodyMode: "multipart",');
|
|
1332
|
+
}
|
|
1333
|
+
if (options.multipartBinaryFields !== undefined) {
|
|
1334
|
+
lines.push(` multipartBinaryFields: ${JSON.stringify(options.multipartBinaryFields)},`);
|
|
1335
|
+
}
|
|
1336
|
+
if (options.contentType !== undefined) {
|
|
1337
|
+
lines.push(` contentType: ${JSON.stringify(options.contentType)},`);
|
|
1338
|
+
}
|
|
903
1339
|
lines.push(" tokenSource,");
|
|
904
1340
|
lines.push(" fetch,");
|
|
905
1341
|
lines.push(" dryRun: params.dryRun,");
|
|
@@ -941,13 +1377,12 @@ function getOperationAuthMode(document, operation, operationId) {
|
|
|
941
1377
|
return security === undefined || security.length === 0 ? "none" : "required";
|
|
942
1378
|
}
|
|
943
1379
|
function assertSupportedHttpMethods(path, pathItem) {
|
|
944
|
-
const rawPathItem = pathItem;
|
|
945
1380
|
for (const method of UNSUPPORTED_HTTP_METHODS) {
|
|
946
|
-
const operation =
|
|
1381
|
+
const operation = getOwnPathItemValue(pathItem, method);
|
|
947
1382
|
if (operation === undefined) {
|
|
948
1383
|
continue;
|
|
949
1384
|
}
|
|
950
|
-
throw new UserError(`Operation ${JSON.stringify(getOperationId(path, method, operation))} uses unsupported HTTP method ${JSON.stringify(method.toUpperCase())}. Supported in v1: GET, POST, PUT, PATCH, DELETE.`);
|
|
1385
|
+
throw new UserError(`Operation ${JSON.stringify(getOperationId(path, method, operation))} uses unsupported HTTP method ${JSON.stringify(method.toUpperCase())}. Supported in v1: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.`);
|
|
951
1386
|
}
|
|
952
1387
|
}
|
|
953
1388
|
function getOperationId(path, method, operation) {
|
|
@@ -988,6 +1423,7 @@ const DEFINITION_RENDERERS = {
|
|
|
988
1423
|
array: (definition, options) => renderSchemaCall("S.Array", renderDefinition(definition.itemDefinition, undefined, undefined, undefined), options),
|
|
989
1424
|
boolean: (_definition, options) => renderSchemaCall("S.Boolean", options),
|
|
990
1425
|
enum: (definition, options) => renderSchemaCall("S.Enum", renderConstArray(definition.enumValues), options),
|
|
1426
|
+
json: (_definition) => renderSchemaCall("S.Json"),
|
|
991
1427
|
number: (_definition, options) => renderSchemaCall("S.Number", options),
|
|
992
1428
|
string: (_definition, options) => renderSchemaCall("S.String", options)
|
|
993
1429
|
};
|
|
@@ -1010,12 +1446,9 @@ function renderObjectKey(name) {
|
|
|
1010
1446
|
}
|
|
1011
1447
|
return JSON.stringify(name);
|
|
1012
1448
|
}
|
|
1013
|
-
function
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
throw new UserError(`Operation ${JSON.stringify(operationId)} derives command noun ${JSON.stringify(noun)}, which maps to invalid TypeScript identifier ${JSON.stringify(identifier)}.`);
|
|
1449
|
+
function createSafeGeneratedNoun(noun) {
|
|
1450
|
+
const normalized = normalizeNoun(noun);
|
|
1451
|
+
return isTypeScriptIdentifier(toCamelCase(normalized)) ? normalized : `api-${normalized || "operation"}`;
|
|
1019
1452
|
}
|
|
1020
1453
|
export function collectSchemaOptionEntries(param) {
|
|
1021
1454
|
return SCHEMA_OPTION_SOURCES.flatMap(({ key, get }) => {
|
|
@@ -1115,8 +1548,17 @@ function resolveQueryArraySerialization(parameter, operationId) {
|
|
|
1115
1548
|
if (style === "pipeDelimited" && explode === false) {
|
|
1116
1549
|
return "pipe";
|
|
1117
1550
|
}
|
|
1551
|
+
if (style === "deepObject" && explode === true) {
|
|
1552
|
+
return "brackets";
|
|
1553
|
+
}
|
|
1118
1554
|
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported query-array serialization for parameter ${JSON.stringify(parameter.name)}. Supported in v1: form (explode true/false) and pipeDelimited.`);
|
|
1119
1555
|
}
|
|
1556
|
+
function resolveQueryObjectSerialization(parameter, operationId) {
|
|
1557
|
+
if (parameter.style === "deepObject" && parameter.explode !== false) {
|
|
1558
|
+
return "deepObject";
|
|
1559
|
+
}
|
|
1560
|
+
throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported query-object serialization for parameter ${JSON.stringify(parameter.name)}. Supported in v1: deepObject with explode true.`);
|
|
1561
|
+
}
|
|
1120
1562
|
function createIndexFile(commands) {
|
|
1121
1563
|
const groups = groupByNoun(commands);
|
|
1122
1564
|
if (groups.length === 0) {
|
|
@@ -1147,6 +1589,22 @@ function createIndexFile(commands) {
|
|
|
1147
1589
|
contents: lines.join("\n")
|
|
1148
1590
|
};
|
|
1149
1591
|
}
|
|
1592
|
+
function createCliFile(theme) {
|
|
1593
|
+
return {
|
|
1594
|
+
path: "cli.ts",
|
|
1595
|
+
contents: [
|
|
1596
|
+
"#!/usr/bin/env node",
|
|
1597
|
+
...createGeneratedTypeScriptFileLines(),
|
|
1598
|
+
'import { configureTheme, runCLI } from "toolcraft/cli";',
|
|
1599
|
+
'import * as groups from "./index.js";',
|
|
1600
|
+
"",
|
|
1601
|
+
`configureTheme({ brand: ${JSON.stringify(theme.brand)}, label: ${JSON.stringify(theme.label)} });`,
|
|
1602
|
+
"",
|
|
1603
|
+
"await runCLI(Object.values(groups));",
|
|
1604
|
+
""
|
|
1605
|
+
].join("\n")
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1150
1608
|
function createGeneratedTypeScriptFile(bodyLines, metadataLines = []) {
|
|
1151
1609
|
return [...createGeneratedTypeScriptFileLines(metadataLines), ...bodyLines].join("\n");
|
|
1152
1610
|
}
|
|
@@ -1166,5 +1624,7 @@ function compareGeneratedCommandPaths(left, right) {
|
|
|
1166
1624
|
return left.verb.localeCompare(right.verb);
|
|
1167
1625
|
}
|
|
1168
1626
|
function isReferenceObject(value) {
|
|
1169
|
-
return typeof value === "object" &&
|
|
1627
|
+
return (typeof value === "object" &&
|
|
1628
|
+
value !== null &&
|
|
1629
|
+
Object.prototype.hasOwnProperty.call(value, "$ref"));
|
|
1170
1630
|
}
|