openapi-typesafe 1.0.0-rc.4 → 1.0.0-rc.5
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/CHANGELOG.md +141 -0
- package/dist/adapters/node-file-system/node-file-system.cjs +7 -0
- package/dist/adapters/node-file-system/node-file-system.cjs.map +1 -1
- package/dist/adapters/node-file-system/node-file-system.mjs +7 -0
- package/dist/adapters/node-file-system/node-file-system.mjs.map +1 -1
- package/dist/adapters/pipe-safe-writer/pipe-safe-writer.cjs +19 -0
- package/dist/adapters/pipe-safe-writer/pipe-safe-writer.cjs.map +1 -0
- package/dist/adapters/pipe-safe-writer/pipe-safe-writer.mjs +17 -0
- package/dist/adapters/pipe-safe-writer/pipe-safe-writer.mjs.map +1 -0
- package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs +5 -32
- package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs.map +1 -1
- package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs +6 -33
- package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs.map +1 -1
- package/dist/adapters/redocly-loader/component-schemas.cjs +17 -0
- package/dist/adapters/redocly-loader/component-schemas.cjs.map +1 -0
- package/dist/adapters/redocly-loader/component-schemas.mjs +14 -0
- package/dist/adapters/redocly-loader/component-schemas.mjs.map +1 -0
- package/dist/adapters/redocly-loader/mapping-schema-names.cjs +22 -0
- package/dist/adapters/redocly-loader/mapping-schema-names.cjs.map +1 -0
- package/dist/adapters/redocly-loader/mapping-schema-names.mjs +20 -0
- package/dist/adapters/redocly-loader/mapping-schema-names.mjs.map +1 -0
- package/dist/adapters/redocly-loader/query-named-components.cjs +137 -0
- package/dist/adapters/redocly-loader/query-named-components.cjs.map +1 -0
- package/dist/adapters/redocly-loader/query-named-components.mjs +134 -0
- package/dist/adapters/redocly-loader/query-named-components.mjs.map +1 -0
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +26 -14
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +26 -14
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.cjs +16 -3
- package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.mjs +16 -3
- package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-holders.cjs +52 -10
- package/dist/adapters/redocly-loader/ref-holders.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-holders.mjs +52 -11
- package/dist/adapters/redocly-loader/ref-holders.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.cjs +5 -3
- package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.mjs +5 -3
- package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-node-limit.cjs +17 -3
- package/dist/adapters/redocly-loader/remote-node-limit.cjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-node-limit.mjs +17 -3
- package/dist/adapters/redocly-loader/remote-node-limit.mjs.map +1 -1
- package/dist/adapters/redocly-loader/schema-source.cjs +7 -2
- package/dist/adapters/redocly-loader/schema-source.cjs.map +1 -1
- package/dist/adapters/redocly-loader/schema-source.mjs +8 -3
- package/dist/adapters/redocly-loader/schema-source.mjs.map +1 -1
- package/dist/adapters/terminal-logger/terminal-logger.cjs +7 -3
- package/dist/adapters/terminal-logger/terminal-logger.cjs.map +1 -1
- package/dist/adapters/terminal-logger/terminal-logger.mjs +7 -3
- package/dist/adapters/terminal-logger/terminal-logger.mjs.map +1 -1
- package/dist/api/document-refs.cjs +16 -1
- package/dist/api/document-refs.cjs.map +1 -1
- package/dist/api/document-refs.mjs +16 -1
- package/dist/api/document-refs.mjs.map +1 -1
- package/dist/api/document-run.cjs +4 -10
- package/dist/api/document-run.cjs.map +1 -1
- package/dist/api/document-run.mjs +4 -10
- package/dist/api/document-run.mjs.map +1 -1
- package/dist/api/openapi-ts.cjs +1 -2
- package/dist/api/openapi-ts.cjs.map +1 -1
- package/dist/api/openapi-ts.mjs +2 -3
- package/dist/api/openapi-ts.mjs.map +1 -1
- package/dist/api/ref-limit-options.cjs +63 -0
- package/dist/api/ref-limit-options.cjs.map +1 -0
- package/dist/api/ref-limit-options.mjs +57 -0
- package/dist/api/ref-limit-options.mjs.map +1 -0
- package/dist/api/transform-schema.cjs +12 -5
- package/dist/api/transform-schema.cjs.map +1 -1
- package/dist/api/transform-schema.mjs +12 -5
- package/dist/api/transform-schema.mjs.map +1 -1
- package/dist/application/transform/declared-value-types.cjs +36 -0
- package/dist/application/transform/declared-value-types.cjs.map +1 -0
- package/dist/application/transform/declared-value-types.mjs +34 -0
- package/dist/application/transform/declared-value-types.mjs.map +1 -0
- package/dist/application/transform/document/document.cjs +3 -1
- package/dist/application/transform/document/document.cjs.map +1 -1
- package/dist/application/transform/document/document.mjs +3 -1
- package/dist/application/transform/document/document.mjs.map +1 -1
- package/dist/application/transform/document/exploded-query-values.cjs +63 -0
- package/dist/application/transform/document/exploded-query-values.cjs.map +1 -0
- package/dist/application/transform/document/exploded-query-values.mjs +59 -0
- package/dist/application/transform/document/exploded-query-values.mjs.map +1 -0
- package/dist/application/transform/document/header.cjs +11 -3
- package/dist/application/transform/document/header.cjs.map +1 -1
- package/dist/application/transform/document/header.mjs +10 -2
- package/dist/application/transform/document/header.mjs.map +1 -1
- package/dist/application/transform/document/parameter.cjs +1 -1
- package/dist/application/transform/document/parameter.cjs.map +1 -1
- package/dist/application/transform/document/parameter.mjs +1 -1
- package/dist/application/transform/document/parameter.mjs.map +1 -1
- package/dist/application/transform/document/parameters.cjs +23 -5
- package/dist/application/transform/document/parameters.cjs.map +1 -1
- package/dist/application/transform/document/parameters.mjs +23 -5
- package/dist/application/transform/document/parameters.mjs.map +1 -1
- package/dist/application/transform/document/path-item.cjs +4 -3
- package/dist/application/transform/document/path-item.cjs.map +1 -1
- package/dist/application/transform/document/path-item.mjs +4 -4
- package/dist/application/transform/document/path-item.mjs.map +1 -1
- package/dist/application/transform/document/path-template-key.cjs +84 -0
- package/dist/application/transform/document/path-template-key.cjs.map +1 -0
- package/dist/application/transform/document/path-template-key.mjs +79 -0
- package/dist/application/transform/document/path-template-key.mjs.map +1 -0
- package/dist/application/transform/document/path-template-refs.cjs +70 -0
- package/dist/application/transform/document/path-template-refs.cjs.map +1 -0
- package/dist/application/transform/document/path-template-refs.mjs +67 -0
- package/dist/application/transform/document/path-template-refs.mjs.map +1 -0
- package/dist/application/transform/document/paths-enum.cjs +7 -6
- package/dist/application/transform/document/paths-enum.cjs.map +1 -1
- package/dist/application/transform/document/paths-enum.mjs +7 -6
- package/dist/application/transform/document/paths-enum.mjs.map +1 -1
- package/dist/application/transform/document/paths.cjs +38 -66
- package/dist/application/transform/document/paths.cjs.map +1 -1
- package/dist/application/transform/document/paths.mjs +36 -66
- package/dist/application/transform/document/paths.mjs.map +1 -1
- package/dist/application/transform/document/webhooks.cjs +10 -3
- package/dist/application/transform/document/webhooks.cjs.map +1 -1
- package/dist/application/transform/document/webhooks.mjs +9 -2
- package/dist/application/transform/document/webhooks.mjs.map +1 -1
- package/dist/application/transform/footer/document-footer.cjs +6 -3
- package/dist/application/transform/footer/document-footer.cjs.map +1 -1
- package/dist/application/transform/footer/document-footer.mjs +6 -3
- package/dist/application/transform/footer/document-footer.mjs.map +1 -1
- package/dist/application/transform/helper-types.cjs +3 -3
- package/dist/application/transform/helper-types.cjs.map +1 -1
- package/dist/application/transform/helper-types.mjs +3 -3
- package/dist/application/transform/helper-types.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-location.cjs +4 -4
- package/dist/application/transform/ref-pointer/generated-location.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-location.mjs +1 -1
- package/dist/application/transform/ref-pointer/generated-location.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.cjs +47 -66
- package/dist/application/transform/ref-pointer/generated-shape.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.mjs +36 -50
- package/dist/application/transform/ref-pointer/generated-shape.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/inline-cycles.cjs +2 -2
- package/dist/application/transform/ref-pointer/inline-cycles.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/inline-cycles.mjs +1 -1
- package/dist/application/transform/ref-pointer/inline-cycles.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/located-node.cjs +23 -0
- package/dist/application/transform/ref-pointer/located-node.cjs.map +1 -0
- package/dist/application/transform/ref-pointer/located-node.mjs +18 -0
- package/dist/application/transform/ref-pointer/located-node.mjs.map +1 -0
- package/dist/application/transform/ref-pointer/original-index.cjs +68 -13
- package/dist/application/transform/ref-pointer/original-index.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/original-index.mjs +70 -15
- package/dist/application/transform/ref-pointer/original-index.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/pointer-type.cjs +2 -2
- package/dist/application/transform/ref-pointer/pointer-type.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/pointer-type.mjs +1 -1
- package/dist/application/transform/ref-pointer/pointer-type.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/pointer-walk.cjs +54 -13
- package/dist/application/transform/ref-pointer/pointer-walk.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/pointer-walk.mjs +46 -7
- package/dist/application/transform/ref-pointer/pointer-walk.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/property-shape.cjs +52 -0
- package/dist/application/transform/ref-pointer/property-shape.cjs.map +1 -0
- package/dist/application/transform/ref-pointer/property-shape.mjs +45 -0
- package/dist/application/transform/ref-pointer/property-shape.mjs.map +1 -0
- package/dist/application/transform/ref-pointer/typed-walk.cjs +79 -21
- package/dist/application/transform/ref-pointer/typed-walk.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/typed-walk.mjs +76 -18
- package/dist/application/transform/ref-pointer/typed-walk.mjs.map +1 -1
- package/dist/application/transform/schema/access-flags.cjs.map +1 -1
- package/dist/application/transform/schema/access-flags.mjs.map +1 -1
- package/dist/application/transform/schema/index-signature-type.cjs +21 -0
- package/dist/application/transform/schema/index-signature-type.cjs.map +1 -0
- package/dist/application/transform/schema/index-signature-type.mjs +19 -0
- package/dist/application/transform/schema/index-signature-type.mjs.map +1 -0
- package/dist/application/transform/schema/input-location.cjs +23 -0
- package/dist/application/transform/schema/input-location.cjs.map +1 -0
- package/dist/application/transform/schema/input-location.mjs +21 -0
- package/dist/application/transform/schema/input-location.mjs.map +1 -0
- package/dist/application/transform/schema/object-type.cjs +15 -5
- package/dist/application/transform/schema/object-type.cjs.map +1 -1
- package/dist/application/transform/schema/object-type.mjs +15 -5
- package/dist/application/transform/schema/object-type.mjs.map +1 -1
- package/dist/application/transform/schema/schema-transform.cjs +2 -1
- package/dist/application/transform/schema/schema-transform.cjs.map +1 -1
- package/dist/application/transform/schema/schema-transform.mjs +2 -1
- package/dist/application/transform/schema/schema-transform.mjs.map +1 -1
- package/dist/application/transform/schema/unevaluated-properties.cjs +2 -30
- package/dist/application/transform/schema/unevaluated-properties.cjs.map +1 -1
- package/dist/application/transform/schema/unevaluated-properties.mjs +3 -31
- package/dist/application/transform/schema/unevaluated-properties.mjs.map +1 -1
- package/dist/cli/api-logger.cjs +13 -0
- package/dist/cli/api-logger.cjs.map +1 -0
- package/dist/cli/api-logger.mjs +11 -0
- package/dist/cli/api-logger.mjs.map +1 -0
- package/dist/cli/api-settings.cjs +7 -15
- package/dist/cli/api-settings.cjs.map +1 -1
- package/dist/cli/api-settings.mjs +8 -16
- package/dist/cli/api-settings.mjs.map +1 -1
- package/dist/cli/cli-arguments.cjs +51 -11
- package/dist/cli/cli-arguments.cjs.map +1 -1
- package/dist/cli/cli-arguments.mjs +51 -11
- package/dist/cli/cli-arguments.mjs.map +1 -1
- package/dist/cli/cli-errors.cjs +17 -0
- package/dist/cli/cli-errors.cjs.map +1 -1
- package/dist/cli/cli-errors.mjs +17 -1
- package/dist/cli/cli-errors.mjs.map +1 -1
- package/dist/cli/cli-output.cjs +6 -4
- package/dist/cli/cli-output.cjs.map +1 -1
- package/dist/cli/cli-output.mjs +7 -5
- package/dist/cli/cli-output.mjs.map +1 -1
- package/dist/cli/main.cjs +10 -4
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.mjs +10 -4
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/redocly-apis.cjs +20 -9
- package/dist/cli/redocly-apis.cjs.map +1 -1
- package/dist/cli/redocly-apis.mjs +22 -11
- package/dist/cli/redocly-apis.mjs.map +1 -1
- package/dist/cli/ref-limits.cjs +15 -1
- package/dist/cli/ref-limits.cjs.map +1 -1
- package/dist/cli/ref-limits.mjs +15 -2
- package/dist/cli/ref-limits.mjs.map +1 -1
- package/dist/cli/run-cli.cjs +24 -7
- package/dist/cli/run-cli.cjs.map +1 -1
- package/dist/cli/run-cli.mjs +25 -8
- package/dist/cli/run-cli.mjs.map +1 -1
- package/dist/domain/cause-secrets.cjs +31 -16
- package/dist/domain/cause-secrets.cjs.map +1 -1
- package/dist/domain/cause-secrets.mjs +31 -16
- package/dist/domain/cause-secrets.mjs.map +1 -1
- package/dist/domain/errors.cjs +2 -0
- package/dist/domain/errors.cjs.map +1 -1
- package/dist/domain/errors.mjs +2 -0
- package/dist/domain/errors.mjs.map +1 -1
- package/dist/domain/location-text.cjs +9 -0
- package/dist/domain/location-text.cjs.map +1 -0
- package/dist/domain/location-text.mjs +7 -0
- package/dist/domain/location-text.mjs.map +1 -0
- package/dist/domain/openapi/document-places.cjs +55 -0
- package/dist/domain/openapi/document-places.cjs.map +1 -0
- package/dist/domain/openapi/document-places.mjs +53 -0
- package/dist/domain/openapi/document-places.mjs.map +1 -0
- package/dist/domain/output-fixes.cjs +8 -2
- package/dist/domain/output-fixes.cjs.map +1 -1
- package/dist/domain/output-fixes.mjs +8 -2
- package/dist/domain/output-fixes.mjs.map +1 -1
- package/dist/domain/terminal-text.cjs +31 -0
- package/dist/domain/terminal-text.cjs.map +1 -0
- package/dist/domain/terminal-text.mjs +29 -0
- package/dist/domain/terminal-text.mjs.map +1 -0
- package/dist/domain/warnings.cjs +1 -0
- package/dist/domain/warnings.cjs.map +1 -1
- package/dist/domain/warnings.mjs +1 -0
- package/dist/domain/warnings.mjs.map +1 -1
- package/dist/index.d.cts +26 -18
- package/dist/index.d.mts +26 -18
- package/dist/index.d.ts +26 -18
- package/package.json +1 -1
- package/src/adapters/node-file-system/node-file-system.ts +14 -1
- package/src/adapters/pipe-safe-writer/pipe-safe-writer.ts +29 -0
- package/src/adapters/redocly-loader/bundled-mapping-copies.ts +7 -42
- package/src/adapters/redocly-loader/component-schemas.ts +19 -0
- package/src/adapters/redocly-loader/mapping-schema-names.ts +25 -0
- package/src/adapters/redocly-loader/query-named-components.ts +201 -0
- package/src/adapters/redocly-loader/redocly-schema-loader.ts +46 -14
- package/src/adapters/redocly-loader/ref-access.ts +16 -3
- package/src/adapters/redocly-loader/ref-holders.ts +75 -14
- package/src/adapters/redocly-loader/remote-fetch.ts +11 -2
- package/src/adapters/redocly-loader/remote-node-limit.ts +44 -4
- package/src/adapters/redocly-loader/schema-source.ts +15 -3
- package/src/adapters/terminal-logger/terminal-logger.ts +10 -4
- package/src/api/document-refs.ts +29 -1
- package/src/api/document-run.ts +6 -16
- package/src/api/openapi-ts.ts +6 -3
- package/src/api/ref-limit-options.ts +76 -0
- package/src/api/transform-schema.ts +20 -5
- package/src/application/ports/file-system.ts +8 -0
- package/src/application/transform/declared-value-types.ts +42 -0
- package/src/application/transform/document/document.ts +4 -2
- package/src/application/transform/document/exploded-query-values.ts +88 -0
- package/src/application/transform/document/header.ts +25 -5
- package/src/application/transform/document/parameter.ts +4 -4
- package/src/application/transform/document/parameters.ts +45 -10
- package/src/application/transform/document/path-item.ts +3 -3
- package/src/application/transform/document/path-template-key.ts +152 -0
- package/src/application/transform/document/path-template-refs.ts +106 -0
- package/src/application/transform/document/paths-enum.ts +23 -9
- package/src/application/transform/document/paths.ts +53 -123
- package/src/application/transform/document/webhooks.ts +18 -5
- package/src/application/transform/footer/document-footer.ts +18 -8
- package/src/application/transform/helper-types.ts +4 -4
- package/src/application/transform/ref-pointer/generated-location.ts +1 -1
- package/src/application/transform/ref-pointer/generated-shape.ts +67 -78
- package/src/application/transform/ref-pointer/inline-cycles.ts +1 -1
- package/src/application/transform/ref-pointer/located-node.ts +38 -0
- package/src/application/transform/ref-pointer/original-index.ts +100 -17
- package/src/application/transform/ref-pointer/pointer-type.ts +1 -1
- package/src/application/transform/ref-pointer/pointer-walk.ts +82 -8
- package/src/application/transform/ref-pointer/property-shape.ts +97 -0
- package/src/application/transform/ref-pointer/typed-walk.ts +117 -25
- package/src/application/transform/schema/access-flags.ts +5 -2
- package/src/application/transform/schema/index-signature-type.ts +31 -0
- package/src/application/transform/schema/input-location.ts +35 -0
- package/src/application/transform/schema/object-type.ts +18 -8
- package/src/application/transform/schema/schema-transform.ts +2 -1
- package/src/application/transform/schema/unevaluated-properties.ts +4 -44
- package/src/cli/api-logger.ts +17 -0
- package/src/cli/api-settings.ts +12 -20
- package/src/cli/cli-arguments.ts +90 -14
- package/src/cli/cli-dependencies.ts +7 -2
- package/src/cli/cli-errors.ts +22 -0
- package/src/cli/cli-output.ts +11 -10
- package/src/cli/main.ts +10 -4
- package/src/cli/redocly-apis.ts +37 -17
- package/src/cli/ref-limits.ts +21 -2
- package/src/cli/run-cli.ts +39 -7
- package/src/domain/cause-secrets.ts +59 -25
- package/src/domain/errors.ts +2 -0
- package/src/domain/location-text.ts +12 -0
- package/src/domain/openapi/document-places.ts +78 -0
- package/src/domain/output-fixes.ts +9 -2
- package/src/domain/terminal-text.ts +44 -0
- package/src/domain/warnings.ts +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendPointer,
|
|
3
|
+
canonicalRef,
|
|
4
|
+
parseRefPointer,
|
|
5
|
+
ROOT_POINTER,
|
|
6
|
+
} from "../../domain/openapi/json-pointer.js";
|
|
7
|
+
import { findMappings, findRefHolders } from "./ref-holders.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Redocly 1.x bundles an external `$ref` into a component named after the last segment of the
|
|
11
|
+
* document's URL, without its extension, when the pointer gives no free name (a `$ref` to a whole
|
|
12
|
+
* document, or two targets with the same pointer). The query stays in that name, and a query holds
|
|
13
|
+
* API keys and signatures, so `https://host/schemas/pet?token=…` became the component
|
|
14
|
+
* `pet?token=…` in the generated file and in messages (threat model T4b). This renames such a
|
|
15
|
+
* component after the URL without its query: `pet`, with a `-2` suffix when that name is taken.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Components = Record<string, Record<string, unknown>>;
|
|
19
|
+
|
|
20
|
+
/** The keys of each `components` group before bundling: the bundle edits the document in place. */
|
|
21
|
+
export type ComponentKeys = ReadonlyMap<string, ReadonlySet<string>>;
|
|
22
|
+
|
|
23
|
+
/** Old and new name of each renamed component, per `components` group. */
|
|
24
|
+
type Renames = Map<string, Map<string, string>>;
|
|
25
|
+
|
|
26
|
+
const COMPONENTS_KEY = "components";
|
|
27
|
+
const REMOTE_URL_RE = /^https?:\/\//;
|
|
28
|
+
const PATH_SEPARATOR_RE = /[/\\]/;
|
|
29
|
+
const EXTENSION_RE = /\.[^.]+$/;
|
|
30
|
+
/** What Redocly puts between the base name and the rest of a component name. */
|
|
31
|
+
const NAME_SUFFIX_STARTS = ["_", "-"];
|
|
32
|
+
const FIRST_SERIAL = 2;
|
|
33
|
+
|
|
34
|
+
function isContainer(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function componentsOf(document: unknown): Components {
|
|
39
|
+
const components = isContainer(document) ? document[COMPONENTS_KEY] : undefined;
|
|
40
|
+
if (!isContainer(components)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
const groups: Components = {};
|
|
44
|
+
for (const [group, entries] of Object.entries(components)) {
|
|
45
|
+
if (isContainer(entries)) {
|
|
46
|
+
groups[group] = entries;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return groups;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The keys of every `components` group of a document. */
|
|
53
|
+
export function componentKeysOf(document: unknown): ComponentKeys {
|
|
54
|
+
const keys = new Map<string, ReadonlySet<string>>();
|
|
55
|
+
for (const [group, entries] of Object.entries(componentsOf(document))) {
|
|
56
|
+
keys.set(group, new Set(Object.keys(entries)));
|
|
57
|
+
}
|
|
58
|
+
return keys;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The base name Redocly 1.34 gives a document (`refBaseName`): the last segment, no extension. */
|
|
62
|
+
function redoclyBaseName(ref: string): string {
|
|
63
|
+
return (ref.split(PATH_SEPARATOR_RE).at(-1) ?? "").replace(EXTENSION_RE, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The base name of the URL without its query and fragment. A URL with no last segment
|
|
68
|
+
* (`https://host/?key=…`) is named after its host, so the name is never empty.
|
|
69
|
+
*/
|
|
70
|
+
function queryFreeBaseName(url: URL): string {
|
|
71
|
+
const withoutQuery = new URL(url.pathname, url.origin).href;
|
|
72
|
+
return redoclyBaseName(withoutQuery) || url.hostname;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Each base name Redocly takes from a remote URL with a query, with the name to use instead. */
|
|
76
|
+
function queryBaseNames(documentRefs: Iterable<string>): Map<string, string> {
|
|
77
|
+
const names = new Map<string, string>();
|
|
78
|
+
for (const ref of documentRefs) {
|
|
79
|
+
const url = REMOTE_URL_RE.test(ref) && URL.canParse(ref) ? new URL(ref) : undefined;
|
|
80
|
+
if (url === undefined || url.search === "") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const named = redoclyBaseName(ref);
|
|
84
|
+
const clean = queryFreeBaseName(url);
|
|
85
|
+
if (named !== clean) {
|
|
86
|
+
names.set(named, clean);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The name `key` gets, or undefined when no query base name starts it. */
|
|
93
|
+
function queryFreeName(key: string, baseNames: ReadonlyMap<string, string>): string | undefined {
|
|
94
|
+
// The longest base name first: `pet?a=1` must not match as `pet?a` when both were loaded.
|
|
95
|
+
const matches = [...baseNames.keys()].filter(
|
|
96
|
+
(named) =>
|
|
97
|
+
key === named || NAME_SUFFIX_STARTS.some((start) => key.startsWith(`${named}${start}`)),
|
|
98
|
+
);
|
|
99
|
+
const named = matches.sort((a, b) => b.length - a.length)[0];
|
|
100
|
+
return named === undefined ? undefined : `${baseNames.get(named)}${key.slice(named.length)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function freeName(name: string, taken: ReadonlySet<string>): string {
|
|
104
|
+
let free = name;
|
|
105
|
+
for (let serial = FIRST_SERIAL; taken.has(free); serial += 1) {
|
|
106
|
+
free = `${name}-${serial}`;
|
|
107
|
+
}
|
|
108
|
+
return free;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Rename the bundled components of one group in place, keeping their order. */
|
|
112
|
+
function renameGroup(
|
|
113
|
+
entries: Record<string, unknown>,
|
|
114
|
+
keysBefore: ReadonlySet<string>,
|
|
115
|
+
baseNames: ReadonlyMap<string, string>,
|
|
116
|
+
): Map<string, string> {
|
|
117
|
+
const renames = new Map<string, string>();
|
|
118
|
+
const taken = new Set(Object.keys(entries));
|
|
119
|
+
for (const key of Object.keys(entries)) {
|
|
120
|
+
const name = keysBefore.has(key) ? undefined : queryFreeName(key, baseNames);
|
|
121
|
+
if (name !== undefined) {
|
|
122
|
+
taken.delete(key);
|
|
123
|
+
const free = freeName(name, taken);
|
|
124
|
+
taken.add(free);
|
|
125
|
+
renames.set(key, free);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
129
|
+
delete entries[key];
|
|
130
|
+
entries[renames.get(key) ?? key] = value;
|
|
131
|
+
}
|
|
132
|
+
return renames;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Rewrites pointers and messages that name a renamed component. */
|
|
136
|
+
export interface ComponentRenaming {
|
|
137
|
+
/** The pointer with a renamed component's new name; any other text as it is. */
|
|
138
|
+
readonly pointer: (pointer: string) => string;
|
|
139
|
+
/** The text with each renamed component's old name replaced by its new name. */
|
|
140
|
+
readonly text: (text: string) => string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renamedPointer(pointer: string, renames: Renames): string {
|
|
144
|
+
if (!pointer.startsWith(`${ROOT_POINTER}/`)) {
|
|
145
|
+
return pointer;
|
|
146
|
+
}
|
|
147
|
+
const [components, group = "", name = "", ...rest] = parseRefPointer(canonicalRef(pointer));
|
|
148
|
+
const renamed = components === COMPONENTS_KEY ? renames.get(group)?.get(name) : undefined;
|
|
149
|
+
return renamed === undefined
|
|
150
|
+
? pointer
|
|
151
|
+
: appendPointer(ROOT_POINTER, [COMPONENTS_KEY, group, renamed, ...rest]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renamedText(text: string, renames: Renames): string {
|
|
155
|
+
const pairs = [...renames.values()].flatMap((group) => [...group]);
|
|
156
|
+
// The longest old name first, so a name that starts another one does not split it.
|
|
157
|
+
pairs.sort(([a], [b]) => b.length - a.length);
|
|
158
|
+
return pairs.reduce((shown, [old, renamed]) => shown.replaceAll(old, renamed), text);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function rewriteRefs(document: unknown, renaming: ComponentRenaming): void {
|
|
162
|
+
for (const { node } of findRefHolders(document)) {
|
|
163
|
+
node.$ref = renaming.pointer(node.$ref);
|
|
164
|
+
}
|
|
165
|
+
for (const mapping of findMappings(document)) {
|
|
166
|
+
for (const [name, value] of Object.entries(mapping)) {
|
|
167
|
+
if (typeof value === "string") {
|
|
168
|
+
mapping[name] = renaming.pointer(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Rename, in place, the components the bundle named after a URL with a query, and every local
|
|
176
|
+
* `$ref` and `mapping` value that names them. `documentRefs` are the refs of the loaded documents.
|
|
177
|
+
*/
|
|
178
|
+
export function renameQueryNamedComponents(
|
|
179
|
+
bundled: unknown,
|
|
180
|
+
keysBefore: ComponentKeys,
|
|
181
|
+
documentRefs: Iterable<string>,
|
|
182
|
+
): ComponentRenaming {
|
|
183
|
+
const baseNames = queryBaseNames(documentRefs);
|
|
184
|
+
const renames: Renames = new Map();
|
|
185
|
+
if (baseNames.size > 0) {
|
|
186
|
+
for (const [group, entries] of Object.entries(componentsOf(bundled))) {
|
|
187
|
+
const groupRenames = renameGroup(entries, keysBefore.get(group) ?? new Set(), baseNames);
|
|
188
|
+
if (groupRenames.size > 0) {
|
|
189
|
+
renames.set(group, groupRenames);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const renaming: ComponentRenaming = {
|
|
194
|
+
pointer: (pointer) => renamedPointer(pointer, renames),
|
|
195
|
+
text: (text) => renamedText(text, renames),
|
|
196
|
+
};
|
|
197
|
+
if (renames.size > 0) {
|
|
198
|
+
rewriteRefs(bundled, renaming);
|
|
199
|
+
}
|
|
200
|
+
return renaming;
|
|
201
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type BaseResolver,
|
|
4
4
|
bundle,
|
|
5
5
|
type Document,
|
|
6
|
+
type LocationObject,
|
|
6
7
|
lintDocument,
|
|
7
8
|
type NormalizedProblem,
|
|
8
9
|
type Config as RedoclyConfig,
|
|
@@ -14,12 +15,19 @@ import type {
|
|
|
14
15
|
SchemaLoadRequest,
|
|
15
16
|
} from "../../application/ports/schema-loader.js";
|
|
16
17
|
import { withStackOverflowReportedAsync } from "../../application/stack-overflow.js";
|
|
18
|
+
import { redactUrlSecrets } from "../../domain/url-secrets.js";
|
|
17
19
|
import { WARNING_CODES, warningText } from "../../domain/warnings.js";
|
|
18
20
|
import type { OpenAPI3 } from "../../types.js";
|
|
19
21
|
import { removeBundledMappingCopies, schemaKeysOf } from "./bundled-mapping-copies.js";
|
|
20
22
|
import { repairBundledRefs } from "./bundled-ref-repair.js";
|
|
21
23
|
import { loadedDocuments, localFilesRead } from "./loaded-documents.js";
|
|
22
24
|
import { findMalformedRefs, malformedRefsError } from "./malformed-refs.js";
|
|
25
|
+
import { pointMappingNamesAtSchemas } from "./mapping-schema-names.js";
|
|
26
|
+
import {
|
|
27
|
+
type ComponentRenaming,
|
|
28
|
+
componentKeysOf,
|
|
29
|
+
renameQueryNamedComponents,
|
|
30
|
+
} from "./query-named-components.js";
|
|
23
31
|
import { RefAccessResolver } from "./ref-access.js";
|
|
24
32
|
import { checkRefsResolve } from "./ref-resolution-check.js";
|
|
25
33
|
import { REF_CYCLE_MESSAGE } from "./ref-resolution-steps.js";
|
|
@@ -160,6 +168,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
160
168
|
const document = await parseSchema(request.source, { absoluteRef, resolver });
|
|
161
169
|
logger.debug("Parsed schema", "redoc", now() - parseStartedAt);
|
|
162
170
|
assertOpenAPI3(document.parsed);
|
|
171
|
+
pointMappingNamesAtSchemas(document.parsed);
|
|
163
172
|
const bundled = await this.#lintAndBundle(document, resolver, request.shouldLint);
|
|
164
173
|
const isRootAFile = request.source instanceof URL && request.source.protocol === "file:";
|
|
165
174
|
const localFiles = await localFilesRead(document, resolver, isRootAFile);
|
|
@@ -171,6 +180,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
171
180
|
const config = resolveConfigWithAccess(redoc.resolve, {
|
|
172
181
|
timeoutMs: remoteRefTimeoutMs,
|
|
173
182
|
access: request.refAccess,
|
|
183
|
+
rootUrl: request.source instanceof URL ? request.source.href : undefined,
|
|
174
184
|
maxParallelFetches: remoteDocumentLimits.maxParallelFetches,
|
|
175
185
|
bodyLimits: remoteDocumentLimits,
|
|
176
186
|
});
|
|
@@ -202,13 +212,14 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
202
212
|
config: this.#options.redoc.styleguide,
|
|
203
213
|
externalRefResolver: resolver,
|
|
204
214
|
});
|
|
205
|
-
this.#report(problems);
|
|
215
|
+
this.#report(problems, document.source.absoluteRef);
|
|
206
216
|
this.#options.logger.debug("Linted schema", "lint", this.#options.now() - startedAt);
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
async #bundle(document: Document, resolver: BaseResolver): Promise<OpenAPI3> {
|
|
210
220
|
const startedAt = this.#options.now();
|
|
211
221
|
const schemaKeys = schemaKeysOf(document.parsed);
|
|
222
|
+
const componentKeys = componentKeysOf(document.parsed);
|
|
212
223
|
// The lint resolver: documents it already loaded are not fetched again.
|
|
213
224
|
const bundled = await bundle({
|
|
214
225
|
config: this.#options.redoc,
|
|
@@ -216,9 +227,12 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
216
227
|
doc: document,
|
|
217
228
|
externalRefResolver: resolver,
|
|
218
229
|
});
|
|
219
|
-
this.#report(bundled.problems);
|
|
220
230
|
removeBundledMappingCopies(bundled.bundle.parsed, schemaKeys);
|
|
221
231
|
repairBundledRefs(bundled.bundle.parsed);
|
|
232
|
+
const documentRefs = resolver.getFiles();
|
|
233
|
+
const renaming = renameQueryNamedComponents(bundled.bundle.parsed, componentKeys, documentRefs);
|
|
234
|
+
// The problems name the components the bundle made, so they are shown by their new names.
|
|
235
|
+
this.#report(bundled.problems, document.source.absoluteRef, renaming);
|
|
222
236
|
this.#options.logger.debug("Bundled schema", "bundle", this.#options.now() - startedAt);
|
|
223
237
|
return bundled.bundle.parsed;
|
|
224
238
|
}
|
|
@@ -228,22 +242,22 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
228
242
|
* config's `.redocly.lint-ignore.yaml` lists is left out, as `redocly lint` leaves it out
|
|
229
243
|
* (upstream #2189).
|
|
230
244
|
*/
|
|
231
|
-
#report(problems: readonly NormalizedProblem[]): void {
|
|
245
|
+
#report(problems: readonly NormalizedProblem[], rootRef: string, renaming = UNRENAMED): void {
|
|
232
246
|
const errorMessages: string[] = [];
|
|
233
247
|
for (const problem of problems) {
|
|
234
248
|
if (problem.ignored === true) {
|
|
235
249
|
this.#options.logger.debug(
|
|
236
|
-
`Ignored by the lint ignore file: ${problemMessage(problem)}`,
|
|
250
|
+
`Ignored by the lint ignore file: ${problemMessage(problem, renaming, rootRef)}`,
|
|
237
251
|
"lint",
|
|
238
252
|
);
|
|
239
253
|
continue;
|
|
240
254
|
}
|
|
241
|
-
const message = problemMessage(problem);
|
|
255
|
+
const message = problemMessage(problem, renaming, rootRef);
|
|
242
256
|
if (problem.severity === "error") {
|
|
243
257
|
errorMessages.push(message);
|
|
244
258
|
this.#options.logger.error(message);
|
|
245
259
|
} else {
|
|
246
|
-
this.#options.logger.warn(lintWarningText(problem));
|
|
260
|
+
this.#options.logger.warn(lintWarningText(message, problem.ruleId));
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
if (errorMessages.length > 0) {
|
|
@@ -253,16 +267,34 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
/** A lint warning names its rule, which `redocly.yaml` can turn off, and ends with its code. */
|
|
256
|
-
function lintWarningText(
|
|
257
|
-
return warningText(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
function lintWarningText(message: string, ruleId: string): string {
|
|
271
|
+
return warningText(`${message}, rule ${ruleId}`, WARNING_CODES.LINT_PROBLEM);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Problems found before the bundle named any component. */
|
|
275
|
+
const UNRENAMED: ComponentRenaming = { pointer: (pointer) => pointer, text: (text) => text };
|
|
276
|
+
|
|
277
|
+
/** A problem location that names a JSON Pointer, not a line and column. */
|
|
278
|
+
type PointerLocation = Extract<LocationObject, { pointer: string }>;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Where a problem is: its pointer, and the document when it is not the root (URL secrets left
|
|
282
|
+
* out).
|
|
283
|
+
*/
|
|
284
|
+
function locationText(location: PointerLocation, renaming: ComponentRenaming, rootRef: string) {
|
|
285
|
+
const pointer = renaming.pointer(location.pointer);
|
|
286
|
+
const documentRef = location.source.absoluteRef;
|
|
287
|
+
return documentRef === rootRef ? pointer : `${pointer} in ${redactUrlSecrets(documentRef)}`;
|
|
261
288
|
}
|
|
262
289
|
|
|
263
|
-
function problemMessage(
|
|
264
|
-
|
|
265
|
-
|
|
290
|
+
function problemMessage(
|
|
291
|
+
problem: NormalizedProblem,
|
|
292
|
+
renaming: ComponentRenaming,
|
|
293
|
+
rootRef: string,
|
|
294
|
+
): string {
|
|
295
|
+
const location = problem.location?.[0];
|
|
296
|
+
const message = renaming.text(problem.message);
|
|
297
|
+
return location?.pointer ? `${message} at ${locationText(location, renaming, rootRef)}` : message;
|
|
266
298
|
}
|
|
267
299
|
|
|
268
300
|
/** A schema loader with one Redocly config and logger, for one or more loads. */
|
|
@@ -13,6 +13,7 @@ import { ERROR_CODES, type OpenAPITypesafeError } from "../../domain/errors.js";
|
|
|
13
13
|
import { redactUrlSecrets } from "../../domain/url-secrets.js";
|
|
14
14
|
import { aliasExpansionFailure } from "./alias-limit.js";
|
|
15
15
|
import { isHostAllowed } from "./allowed-hosts.js";
|
|
16
|
+
import { pointMappingNamesAtSchemas } from "./mapping-schema-names.js";
|
|
16
17
|
import { isParsedSource, RemoteNodeLimit } from "./remote-node-limit.js";
|
|
17
18
|
import { refForbiddenError } from "./schema-load-errors.js";
|
|
18
19
|
|
|
@@ -162,21 +163,33 @@ export class RefAccessResolver extends BaseResolver {
|
|
|
162
163
|
return super.resolveDocument(base, ref, isRoot);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Refuse a YAML alias bomb before Redocly walks it (pre-beta test plan 10.2). A document a
|
|
168
|
+
* `$ref` loads has its own `components.schemas`, so its mapping names point at those schemas
|
|
169
|
+
* before Redocly resolves them, as the root's do (`pointMappingNamesAtSchemas`).
|
|
170
|
+
*/
|
|
166
171
|
override parseDocument(source: Source, isRoot = false): Document {
|
|
167
172
|
const document = this.#parsed(source, isRoot);
|
|
168
173
|
const failure = aliasExpansionFailure(document.parsed);
|
|
169
174
|
if (failure !== undefined) {
|
|
170
175
|
throw new YamlParseError(failure, source);
|
|
171
176
|
}
|
|
177
|
+
pointMappingNamesAtSchemas(document.parsed);
|
|
172
178
|
return document;
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* A remote document is parsed within the node limit; a local one as Redocly parses it. Whether
|
|
183
|
+
* a remote one is parsed at all is decided here (`isParsedSource`), since Redocly read the
|
|
184
|
+
* extension of a URL with its query.
|
|
185
|
+
*/
|
|
176
186
|
#parsed(source: Source, isRoot: boolean): Document {
|
|
177
|
-
if (!isRemoteRef(source.absoluteRef)
|
|
187
|
+
if (!isRemoteRef(source.absoluteRef)) {
|
|
178
188
|
return super.parseDocument(source, isRoot);
|
|
179
189
|
}
|
|
190
|
+
if (!isParsedSource(source, isRoot)) {
|
|
191
|
+
return { source, parsed: source.body };
|
|
192
|
+
}
|
|
180
193
|
try {
|
|
181
194
|
return this.#nodeLimit.parse(source);
|
|
182
195
|
} catch (error) {
|
|
@@ -6,38 +6,99 @@ export interface RefHolder {
|
|
|
6
6
|
readonly pointer: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Keywords whose value is data, not OpenAPI: a `$ref` or a `discriminator` in an example, a
|
|
11
|
+
* default, a `const` or an `enum` value, or an extension is part of the data and stays as written.
|
|
12
|
+
*/
|
|
13
|
+
const DATA_KEYWORDS = new Set(["example", "examples", "default", "const", "enum"]);
|
|
14
|
+
const EXTENSION_PREFIX = "x-";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Keywords whose value maps names to objects: a property, a schema or a response may be named
|
|
18
|
+
* `default` or `example`, and is OpenAPI all the same.
|
|
19
|
+
*/
|
|
20
|
+
const NAME_MAPS = new Set([
|
|
21
|
+
"properties",
|
|
22
|
+
"patternProperties",
|
|
23
|
+
"dependentSchemas",
|
|
24
|
+
"$defs",
|
|
25
|
+
"definitions",
|
|
26
|
+
"schemas",
|
|
27
|
+
"responses",
|
|
28
|
+
"parameters",
|
|
29
|
+
"requestBodies",
|
|
30
|
+
"headers",
|
|
31
|
+
"securitySchemes",
|
|
32
|
+
"links",
|
|
33
|
+
"callbacks",
|
|
34
|
+
"pathItems",
|
|
35
|
+
"mediaTypes",
|
|
36
|
+
"content",
|
|
37
|
+
"encoding",
|
|
38
|
+
"paths",
|
|
39
|
+
"webhooks",
|
|
40
|
+
]);
|
|
41
|
+
|
|
9
42
|
interface PendingNode {
|
|
10
43
|
readonly value: object;
|
|
11
44
|
readonly pointer: string;
|
|
45
|
+
/** Whether the keys of `value` are names (of properties, schemas, responses), not keywords. */
|
|
46
|
+
readonly hasNameKeys: boolean;
|
|
12
47
|
}
|
|
13
48
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
49
|
+
function isDataKey(key: string): boolean {
|
|
50
|
+
return DATA_KEYWORDS.has(key) || key.startsWith(EXTENSION_PREFIX);
|
|
16
51
|
}
|
|
17
52
|
|
|
18
53
|
/**
|
|
19
|
-
* Every object
|
|
20
|
-
*
|
|
21
|
-
* YAML aliases (or a cycle of them) is visited
|
|
54
|
+
* Every object of a parsed document that is OpenAPI, not data (`DATA_KEYWORDS`), with its JSON
|
|
55
|
+
* Pointer, in no particular order. The walk uses its own stack, so a deeply nested document does
|
|
56
|
+
* not run out of call stack, and a node shared by YAML aliases (or a cycle of them) is visited
|
|
57
|
+
* once.
|
|
22
58
|
*/
|
|
23
|
-
|
|
24
|
-
const found:
|
|
59
|
+
function openApiNodes(document: unknown): PendingNode[] {
|
|
60
|
+
const found: PendingNode[] = [];
|
|
25
61
|
const visited = new WeakSet<object>();
|
|
26
62
|
const pending: PendingNode[] = [];
|
|
27
|
-
const enqueue = (value: unknown, pointer: string): void => {
|
|
63
|
+
const enqueue = (value: unknown, pointer: string, hasNameKeys: boolean): void => {
|
|
28
64
|
if (typeof value === "object" && value !== null && !visited.has(value)) {
|
|
29
65
|
visited.add(value);
|
|
30
|
-
pending.push({ value, pointer });
|
|
66
|
+
pending.push({ value, pointer, hasNameKeys });
|
|
31
67
|
}
|
|
32
68
|
};
|
|
33
|
-
enqueue(document, ROOT_POINTER);
|
|
69
|
+
enqueue(document, ROOT_POINTER, false);
|
|
34
70
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
71
|
+
found.push(next);
|
|
72
|
+
// One push per child: spreading an array of millions of children overflows the stack.
|
|
38
73
|
for (const [key, child] of Object.entries(next.value)) {
|
|
39
|
-
|
|
74
|
+
if (next.hasNameKeys || !isDataKey(key)) {
|
|
75
|
+
enqueue(child, childPointer(next.pointer, key), !next.hasNameKeys && NAME_MAPS.has(key));
|
|
76
|
+
}
|
|
40
77
|
}
|
|
41
78
|
}
|
|
42
79
|
return found;
|
|
43
80
|
}
|
|
81
|
+
|
|
82
|
+
function hasStringRef(value: object): value is { $ref: string } {
|
|
83
|
+
return !Array.isArray(value) && typeof (value as { $ref?: unknown }).$ref === "string";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every object with a string `$ref` in a parsed document, outside data, in no particular order. */
|
|
87
|
+
export function findRefHolders(document: unknown): RefHolder[] {
|
|
88
|
+
return openApiNodes(document).flatMap(({ value, pointer, hasNameKeys }) =>
|
|
89
|
+
!hasNameKeys && hasStringRef(value) ? [{ node: value, pointer }] : [],
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isMapping(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every `discriminator.mapping` object of a parsed document, outside data. */
|
|
98
|
+
export function findMappings(document: unknown): Record<string, unknown>[] {
|
|
99
|
+
return openApiNodes(document).flatMap(({ value, hasNameKeys }) => {
|
|
100
|
+
const { discriminator } = value as { discriminator?: unknown };
|
|
101
|
+
const mapping = !hasNameKeys && isMapping(discriminator) ? discriminator.mapping : undefined;
|
|
102
|
+
return isMapping(mapping) ? [mapping] : [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -115,6 +115,11 @@ export interface RedirectPolicy {
|
|
|
115
115
|
readonly headers: readonly ConfiguredHeader[];
|
|
116
116
|
/** `--remote-ref-hosts`; undefined allows every host. */
|
|
117
117
|
readonly hosts: readonly string[] | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The URL of the root document, if it is remote. The developer chose it, so neither it nor its
|
|
120
|
+
* redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.
|
|
121
|
+
*/
|
|
122
|
+
readonly rootUrl: string | undefined;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/**
|
|
@@ -130,12 +135,13 @@ export function fetchWithCheckedRedirects(
|
|
|
130
135
|
policy: RedirectPolicy,
|
|
131
136
|
): RemoteFetch {
|
|
132
137
|
return async (input, init = {}) => {
|
|
138
|
+
const hosts = input === policy.rootUrl ? undefined : policy.hosts;
|
|
133
139
|
let url = new URL(input);
|
|
134
140
|
for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
|
|
135
|
-
if (hop > 0 &&
|
|
141
|
+
if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {
|
|
136
142
|
throw refForbiddenError(url.href, {
|
|
137
143
|
reason: "host-not-allowed",
|
|
138
|
-
allowedHosts: [...
|
|
144
|
+
allowedHosts: [...hosts],
|
|
139
145
|
});
|
|
140
146
|
}
|
|
141
147
|
const headers = await headersFor(url, policy.headers);
|
|
@@ -190,6 +196,8 @@ export interface RemoteFetchOptions {
|
|
|
190
196
|
/** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */
|
|
191
197
|
readonly timeoutMs: number;
|
|
192
198
|
readonly access: RefAccess;
|
|
199
|
+
/** The root document's URL, whose redirects are not limited to the hosts. */
|
|
200
|
+
readonly rootUrl: string | undefined;
|
|
193
201
|
readonly maxParallelFetches: number;
|
|
194
202
|
readonly bodyLimits: RemoteBodyLimits;
|
|
195
203
|
}
|
|
@@ -216,6 +224,7 @@ export function resolveConfigWithAccess(
|
|
|
216
224
|
const checked = fetchWithCheckedRedirects(base, {
|
|
217
225
|
headers: resolve.http.headers,
|
|
218
226
|
hosts: options.access.remoteHosts,
|
|
227
|
+
rootUrl: options.rootUrl,
|
|
219
228
|
});
|
|
220
229
|
// The limit is outside the redirects, so all hops of one document share one slot.
|
|
221
230
|
const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);
|
|
@@ -13,12 +13,52 @@ import { refForbiddenError } from "./schema-load-errors.js";
|
|
|
13
13
|
/** The extensions that Redocly 1.34's `BaseResolver.parseDocument` parses as YAML or JSON. */
|
|
14
14
|
const PARSED_EXTENSIONS: ReadonlySet<string> = new Set([".json", ".yml", ".yaml"]);
|
|
15
15
|
const PARSED_MIME_TYPE = /(json|yaml|openapi)/;
|
|
16
|
+
/**
|
|
17
|
+
* The text formats that a `$ref` loads as text, as a `description` from a Markdown file. A URL
|
|
18
|
+
* keeps its document as text only when its extension or type names one of them: the last segment
|
|
19
|
+
* of an API's path often has a dot that is no file type (`/schemas/v1.0`, `/schemas/pet.v2`).
|
|
20
|
+
*/
|
|
21
|
+
const TEXT_EXTENSIONS: ReadonlySet<string> = new Set([".md", ".markdown", ".txt", ".html", ".htm"]);
|
|
22
|
+
const TEXT_MIME_TYPE = /^text\/(markdown|x-markdown|html)\b/;
|
|
23
|
+
const REMOTE_URL_RE = /^https?:\/\//;
|
|
24
|
+
const PATH_SEPARATOR_RE = /[/\\]/;
|
|
25
|
+
const EXTENSION_START = ".";
|
|
26
|
+
|
|
27
|
+
/** The extension of the last segment of a path, with its dot; empty when it has none. */
|
|
28
|
+
function extensionOf(path: string): string {
|
|
29
|
+
const name = path.split(PATH_SEPARATOR_RE).at(-1) ?? "";
|
|
30
|
+
const start = name.lastIndexOf(EXTENSION_START);
|
|
31
|
+
return start === -1 ? "" : name.slice(start);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The path of a document: the path of a URL, without its query and fragment, or the file path.
|
|
36
|
+
* Redocly took the extension of the whole URL, so `pet.yaml?token=…` had none it knew.
|
|
37
|
+
*/
|
|
38
|
+
function documentPath(absoluteRef: string): string {
|
|
39
|
+
return REMOTE_URL_RE.test(absoluteRef) && URL.canParse(absoluteRef)
|
|
40
|
+
? new URL(absoluteRef).pathname
|
|
41
|
+
: absoluteRef;
|
|
42
|
+
}
|
|
16
43
|
|
|
17
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Whether a source is parsed as YAML or JSON, or kept as text (a Markdown `description`, say).
|
|
46
|
+
* It is parsed when it is the root, when its type or the extension of its path says JSON or YAML,
|
|
47
|
+
* and when it is a URL whose path and type name no text format: an API serves its documents at
|
|
48
|
+
* paths like `/schemas/pet` or `/schemas/v1.0`, often as `text/plain`, and Redocly kept those as
|
|
49
|
+
* text, which failed as a schema. A local file is kept as text unless its name says JSON or YAML,
|
|
50
|
+
* as Redocly keeps it.
|
|
51
|
+
*/
|
|
18
52
|
export function isParsedSource(source: Source, isRoot: boolean): boolean {
|
|
19
|
-
const { absoluteRef, mimeType } = source;
|
|
20
|
-
const extension =
|
|
21
|
-
|
|
53
|
+
const { absoluteRef, mimeType = "" } = source;
|
|
54
|
+
const extension = extensionOf(documentPath(absoluteRef));
|
|
55
|
+
const isTextUrl = TEXT_EXTENSIONS.has(extension) || TEXT_MIME_TYPE.test(mimeType);
|
|
56
|
+
return (
|
|
57
|
+
isRoot ||
|
|
58
|
+
PARSED_EXTENSIONS.has(extension) ||
|
|
59
|
+
PARSED_MIME_TYPE.test(mimeType) ||
|
|
60
|
+
(REMOTE_URL_RE.test(absoluteRef) && !isTextUrl)
|
|
61
|
+
);
|
|
22
62
|
}
|
|
23
63
|
|
|
24
64
|
/** The parser's event for a node it starts to build. */
|
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import {
|
|
5
5
|
type BaseResolver,
|
|
6
6
|
type Document,
|
|
7
|
-
|
|
7
|
+
parseYaml,
|
|
8
8
|
ResolveError,
|
|
9
9
|
Source,
|
|
10
10
|
YamlParseError,
|
|
@@ -74,12 +74,24 @@ function parseJsonText(text: string, absoluteRef: string): Document {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* How a parser message names document text that is no file. The document itself is named as a
|
|
79
|
+
* file in the working directory, so that its relative `$ref`s resolve there, but no such file
|
|
80
|
+
* exists, and a message that named it sent the user looking for it.
|
|
81
|
+
*/
|
|
82
|
+
const TEXT_ORIGIN_NAMES: Readonly<Record<TextOrigin, string>> = {
|
|
83
|
+
string: "the schema string",
|
|
84
|
+
stream: "the schema stream",
|
|
85
|
+
};
|
|
86
|
+
|
|
77
87
|
function parseYamlText(text: string, absoluteRef: string, origin: TextOrigin): Document {
|
|
88
|
+
const source = new Source(absoluteRef, text);
|
|
78
89
|
let document: Document;
|
|
79
90
|
try {
|
|
80
|
-
document =
|
|
91
|
+
document = { source, parsed: parseYaml(text, { filename: TEXT_ORIGIN_NAMES[origin] }) };
|
|
81
92
|
} catch (cause) {
|
|
82
|
-
|
|
93
|
+
// As Redocly's own parse: the error keeps the first line, not the parser's code excerpt.
|
|
94
|
+
throw parseFailedError("yaml", absoluteRef, new YamlParseError(cause as Error, source));
|
|
83
95
|
}
|
|
84
96
|
// Text that is only a YAML string scalar, such as "./openapi.yaml", was meant as a file path.
|
|
85
97
|
if (typeof document.parsed === "string") {
|