openapi-typesafe 1.0.0-rc.5 → 1.0.0-rc.6
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 +73 -0
- package/dist/adapters/redocly-loader/query-document-names.cjs +50 -0
- package/dist/adapters/redocly-loader/query-document-names.cjs.map +1 -0
- package/dist/adapters/redocly-loader/query-document-names.mjs +48 -0
- package/dist/adapters/redocly-loader/query-document-names.mjs.map +1 -0
- package/dist/adapters/redocly-loader/query-named-components.cjs +29 -37
- package/dist/adapters/redocly-loader/query-named-components.cjs.map +1 -1
- package/dist/adapters/redocly-loader/query-named-components.mjs +29 -37
- package/dist/adapters/redocly-loader/query-named-components.mjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-config-source.cjs +3 -0
- package/dist/adapters/redocly-loader/redocly-config-source.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-config-source.mjs +4 -1
- package/dist/adapters/redocly-loader/redocly-config-source.mjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +16 -7
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +17 -8
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.cjs +9 -1
- package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.mjs +10 -2
- package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.cjs +16 -1
- package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.mjs +16 -2
- package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
- package/dist/api/cwd-option.cjs +58 -0
- package/dist/api/cwd-option.cjs.map +1 -0
- package/dist/api/cwd-option.mjs +52 -0
- package/dist/api/cwd-option.mjs.map +1 -0
- package/dist/api/default-ports.cjs +15 -7
- package/dist/api/default-ports.cjs.map +1 -1
- package/dist/api/default-ports.mjs +14 -8
- package/dist/api/default-ports.mjs.map +1 -1
- package/dist/api/document-run.cjs +2 -16
- package/dist/api/document-run.cjs.map +1 -1
- package/dist/api/document-run.mjs +2 -12
- package/dist/api/document-run.mjs.map +1 -1
- package/dist/api/legacy-utils.cjs +1 -1
- package/dist/api/legacy-utils.cjs.map +1 -1
- package/dist/api/legacy-utils.mjs +1 -1
- package/dist/api/legacy-utils.mjs.map +1 -1
- package/dist/api/openapi-ts.cjs.map +1 -1
- package/dist/api/openapi-ts.mjs.map +1 -1
- package/dist/api/option-errors.cjs +24 -0
- package/dist/api/option-errors.cjs.map +1 -0
- package/dist/api/option-errors.mjs +21 -0
- package/dist/api/option-errors.mjs.map +1 -0
- package/dist/api/ref-limit-options.cjs +19 -24
- package/dist/api/ref-limit-options.cjs.map +1 -1
- package/dist/api/ref-limit-options.mjs +15 -20
- package/dist/api/ref-limit-options.mjs.map +1 -1
- package/dist/application/transform/document/path-template-refs.cjs +12 -2
- package/dist/application/transform/document/path-template-refs.cjs.map +1 -1
- package/dist/application/transform/document/path-template-refs.mjs +12 -2
- package/dist/application/transform/document/path-template-refs.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.cjs +28 -10
- package/dist/application/transform/ref-pointer/generated-shape.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.mjs +29 -11
- package/dist/application/transform/ref-pointer/generated-shape.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/original-index.cjs +1 -1
- package/dist/application/transform/ref-pointer/original-index.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/original-index.mjs +1 -1
- package/dist/application/transform/ref-pointer/original-index.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/property-shape.cjs +5 -0
- package/dist/application/transform/ref-pointer/property-shape.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/property-shape.mjs +5 -1
- package/dist/application/transform/ref-pointer/property-shape.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/typed-walk.cjs +11 -1
- package/dist/application/transform/ref-pointer/typed-walk.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/typed-walk.mjs +12 -2
- package/dist/application/transform/ref-pointer/typed-walk.mjs.map +1 -1
- package/dist/cli/api-settings.cjs +3 -2
- package/dist/cli/api-settings.cjs.map +1 -1
- package/dist/cli/api-settings.mjs +2 -1
- package/dist/cli/api-settings.mjs.map +1 -1
- package/dist/cli/api-writes.cjs +7 -1
- package/dist/cli/api-writes.cjs.map +1 -1
- package/dist/cli/api-writes.mjs +7 -1
- package/dist/cli/api-writes.mjs.map +1 -1
- package/dist/cli/cli-arguments.cjs +25 -107
- package/dist/cli/cli-arguments.cjs.map +1 -1
- package/dist/cli/cli-arguments.mjs +18 -100
- package/dist/cli/cli-arguments.mjs.map +1 -1
- package/dist/cli/cli-errors.cjs +1 -0
- package/dist/cli/cli-errors.cjs.map +1 -1
- package/dist/cli/cli-errors.mjs +1 -0
- package/dist/cli/cli-errors.mjs.map +1 -1
- package/dist/cli/cli-flag-names.cjs +77 -0
- package/dist/cli/cli-flag-names.cjs.map +1 -0
- package/dist/cli/cli-flag-names.mjs +64 -0
- package/dist/cli/cli-flag-names.mjs.map +1 -0
- package/dist/cli/cli-output.cjs +7 -2
- package/dist/cli/cli-output.cjs.map +1 -1
- package/dist/cli/cli-output.mjs +7 -2
- package/dist/cli/cli-output.mjs.map +1 -1
- package/dist/cli/main.cjs +1 -1
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.mjs +1 -1
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/redocly-apis.cjs +14 -4
- package/dist/cli/redocly-apis.cjs.map +1 -1
- package/dist/cli/redocly-apis.mjs +14 -4
- package/dist/cli/redocly-apis.mjs.map +1 -1
- package/dist/cli/written-flags.cjs +83 -0
- package/dist/cli/written-flags.cjs.map +1 -0
- package/dist/cli/written-flags.mjs +77 -0
- package/dist/cli/written-flags.mjs.map +1 -0
- package/dist/domain/errors.cjs +1 -0
- package/dist/domain/errors.cjs.map +1 -1
- package/dist/domain/errors.mjs +1 -0
- package/dist/domain/errors.mjs.map +1 -1
- package/dist/domain/location-text.cjs +7 -1
- package/dist/domain/location-text.cjs.map +1 -1
- package/dist/domain/location-text.mjs +7 -1
- package/dist/domain/location-text.mjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/adapters/redocly-loader/query-document-names.ts +79 -0
- package/src/adapters/redocly-loader/query-named-components.ts +48 -49
- package/src/adapters/redocly-loader/redocly-config-source.ts +11 -1
- package/src/adapters/redocly-loader/redocly-schema-loader.ts +20 -6
- package/src/adapters/redocly-loader/ref-access.ts +16 -2
- package/src/adapters/redocly-loader/remote-fetch.ts +26 -0
- package/src/api/cwd-option.ts +76 -0
- package/src/api/default-ports.ts +33 -10
- package/src/api/document-run.ts +2 -17
- package/src/api/legacy-utils.ts +1 -1
- package/src/api/openapi-ts.ts +14 -1
- package/src/api/option-errors.ts +22 -0
- package/src/api/ref-limit-options.ts +17 -24
- package/src/application/ports/redocly-config-source.ts +5 -0
- package/src/application/transform/document/path-template-refs.ts +28 -7
- package/src/application/transform/ref-pointer/generated-shape.ts +57 -14
- package/src/application/transform/ref-pointer/original-index.ts +6 -3
- package/src/application/transform/ref-pointer/property-shape.ts +9 -0
- package/src/application/transform/ref-pointer/typed-walk.ts +26 -4
- package/src/cli/api-writes.ts +11 -3
- package/src/cli/cli-arguments.ts +42 -153
- package/src/cli/cli-errors.ts +1 -0
- package/src/cli/cli-flag-names.ts +99 -0
- package/src/cli/cli-output.ts +20 -6
- package/src/cli/main.ts +2 -1
- package/src/cli/redocly-apis.ts +21 -5
- package/src/cli/written-flags.ts +131 -0
- package/src/domain/errors.ts +1 -0
- package/src/domain/location-text.ts +17 -2
package/src/api/openapi-ts.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createRedoclySchemaLoader } from "../adapters/redocly-loader/redocly-sc
|
|
|
2
2
|
import type { Logger } from "../application/ports/logger.js";
|
|
3
3
|
import { withStackOverflowReported } from "../application/stack-overflow.js";
|
|
4
4
|
import type { IrDeclaration } from "../domain/ir/nodes.js";
|
|
5
|
-
import type { GeneratorOptions, SchemaSource } from "../types.js";
|
|
5
|
+
import type { GeneratorOptions, OpenAPITSOptions, SchemaSource } from "../types.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_REMOTE_DOCUMENT_LIMITS,
|
|
8
8
|
defaultDeprecationReporter,
|
|
@@ -78,6 +78,19 @@ export async function defaultGeneratorPorts(
|
|
|
78
78
|
* - URL: URL to a YAML or JSON file (local or remote)
|
|
79
79
|
* - Readable: Readable stream of YAML or JSON
|
|
80
80
|
*/
|
|
81
|
+
export default function openapiTS(
|
|
82
|
+
source: SchemaSource,
|
|
83
|
+
options?: OpenAPITSOptions,
|
|
84
|
+
): Promise<IrDeclaration[]>;
|
|
85
|
+
// The options without `irCallbacks` get their own signature: TypeScript does not pick a member of
|
|
86
|
+
// `GeneratorOptions | undefined` (an optional parameter) by a key that the object leaves out, so
|
|
87
|
+
// inline callbacks had implicit `any` parameters (TS7006). `irCallbacks: true` picks the member
|
|
88
|
+
// here. This signature is last, so `Parameters<typeof openapiTS>` reads it.
|
|
89
|
+
/** Convert an OpenAPI schema to IR declarations, with either form of the options. */
|
|
90
|
+
export default function openapiTS(
|
|
91
|
+
source: SchemaSource,
|
|
92
|
+
options?: GeneratorOptions,
|
|
93
|
+
): Promise<IrDeclaration[]>;
|
|
81
94
|
export default async function openapiTS(
|
|
82
95
|
source: SchemaSource,
|
|
83
96
|
options: GeneratorOptions = {},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ERROR_CODES, OpenAPITypesafeError } from "../domain/errors.js";
|
|
2
|
+
|
|
3
|
+
/** The `OPTION_INVALID` errors of the Node API's options, checked before anything loads. */
|
|
4
|
+
|
|
5
|
+
/** What a value is, for the error; a URL is named by its protocol only, as it may hold secrets. */
|
|
6
|
+
export function kindOf(value: unknown): string {
|
|
7
|
+
if (value instanceof URL) {
|
|
8
|
+
return `a ${value.protocol} URL`;
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return "a list";
|
|
12
|
+
}
|
|
13
|
+
return value === null ? "null" : `a ${typeof value}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function optionInvalid(option: string, expected: string, got: string): OpenAPITypesafeError {
|
|
17
|
+
return new OpenAPITypesafeError({
|
|
18
|
+
code: ERROR_CODES.OPTION_INVALID,
|
|
19
|
+
message: `The ${option} option must be ${expected}; got ${got}.`,
|
|
20
|
+
details: { option },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import type { RefAccess } from "../application/ports/schema-loader.js";
|
|
4
|
-
import { ERROR_CODES, OpenAPITypesafeError } from "../domain/errors.js";
|
|
5
4
|
import { hasUrlScheme } from "../domain/location-text.js";
|
|
6
5
|
import type { GeneratorOptions } from "../types.js";
|
|
6
|
+
import { kindOf, optionInvalid } from "./option-errors.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The `$ref` limits of the options (threat model R1, R2, ADR-017), checked before anything loads.
|
|
@@ -13,36 +13,29 @@ import type { GeneratorOptions } from "../types.js";
|
|
|
13
13
|
|
|
14
14
|
const FILE_PROTOCOL = "file:";
|
|
15
15
|
|
|
16
|
-
/** What a value is, for the error; a URL is named by its protocol only, as it may hold secrets. */
|
|
17
|
-
function kindOf(value: unknown): string {
|
|
18
|
-
if (value instanceof URL) {
|
|
19
|
-
return `a ${value.protocol} URL`;
|
|
20
|
-
}
|
|
21
|
-
if (Array.isArray(value)) {
|
|
22
|
-
return "a list";
|
|
23
|
-
}
|
|
24
|
-
return value === null ? "null" : `a ${typeof value}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function optionInvalid(option: string, expected: string, got: string): OpenAPITypesafeError {
|
|
28
|
-
return new OpenAPITypesafeError({
|
|
29
|
-
code: ERROR_CODES.OPTION_INVALID,
|
|
30
|
-
message: `The ${option} option must be ${expected}; got ${got}.`,
|
|
31
|
-
details: { option },
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
16
|
const LOCAL_ROOT_EXPECTED = "a directory path or a file: URL";
|
|
17
|
+
const RELATIVE_ROOT_EXPECTED = "an absolute path or a file: URL when cwd is no file: URL";
|
|
36
18
|
|
|
37
19
|
/**
|
|
38
20
|
* A string as the CLI reads `--local-ref-root`: a URL when it has a scheme (`file:///specs/`),
|
|
39
|
-
* else a path that resolves against the cwd. Undefined when it does not parse as a URL.
|
|
21
|
+
* else a path that resolves against the cwd. Undefined when it does not parse as a URL. A
|
|
22
|
+
* relative path needs a local cwd: with an `http:` cwd it names no local directory.
|
|
40
23
|
*/
|
|
41
24
|
function localRootUrlOf(value: string, cwd: URL): URL | undefined {
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
25
|
+
if (hasUrlScheme(value)) {
|
|
26
|
+
return URL.parse(value, cwd.href) ?? undefined;
|
|
27
|
+
}
|
|
28
|
+
if (path.isAbsolute(value)) {
|
|
29
|
+
return pathToFileURL(value);
|
|
30
|
+
}
|
|
31
|
+
if (cwd.protocol !== FILE_PROTOCOL) {
|
|
32
|
+
throw optionInvalid(
|
|
33
|
+
"localRefRoot",
|
|
34
|
+
RELATIVE_ROOT_EXPECTED,
|
|
35
|
+
`a relative path, and cwd is ${kindOf(cwd)}`,
|
|
36
|
+
);
|
|
44
37
|
}
|
|
45
|
-
return
|
|
38
|
+
return pathToFileURL(path.resolve(fileURLToPath(cwd), value));
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
/** The local root as a `file:` URL; the option is set, so it is not undefined. */
|
|
@@ -17,4 +17,9 @@ export interface RedoclyConfigSource {
|
|
|
17
17
|
* when the ignore file of the working directory is bad.
|
|
18
18
|
*/
|
|
19
19
|
createDefault(): Promise<RedoclyConfig>;
|
|
20
|
+
/**
|
|
21
|
+
* The config one API of `config` is linted with: the API's own `rules` (and other lint
|
|
22
|
+
* settings) under `apis.<name>` in place of the root's, as `redocly lint <name>` reads them.
|
|
23
|
+
*/
|
|
24
|
+
forApi(config: RedoclyConfig, apiName: string): RedoclyConfig;
|
|
20
25
|
}
|
|
@@ -2,6 +2,7 @@ import { irRaw } from "../../../domain/ir/constructors.js";
|
|
|
2
2
|
import type { IrDeclaration } from "../../../domain/ir/nodes.js";
|
|
3
3
|
import type { PathsObject } from "../../../domain/openapi/path-item-object.js";
|
|
4
4
|
import { documentEntries } from "../document-entries.js";
|
|
5
|
+
import { HELPER_TYPES } from "../helper-types.js";
|
|
5
6
|
import type { TransformContext } from "../transform-context.js";
|
|
6
7
|
import type { PathKeyPart } from "./path-key-overlaps.js";
|
|
7
8
|
import { booleanExpandedKeys, pathTemplateParts, templateText } from "./path-template-key.js";
|
|
@@ -11,7 +12,8 @@ import { isUntakenKey, pathKeyOf } from "./paths.js";
|
|
|
11
12
|
* `--path-params-as-types` and `$ref`s into a templated path. Such a path is an index signature
|
|
12
13
|
* of `paths` (`` [path: `/pets/${number}`] ``), so `paths["/pets/{id}"]`, the type of a `$ref`
|
|
13
14
|
* to it, names no key: `{id}` is no number (TS2339). The reference indexes `paths` by the
|
|
14
|
-
* template instead, which reaches the signature
|
|
15
|
+
* template instead, which reaches the signature; so does the type of an `--enum-values` array,
|
|
16
|
+
* which indexes `FlattenedDeepRequired<paths>`. With only `${string}` placeholders the URL
|
|
15
17
|
* itself matches the template and the output compiles, so it stays as it is.
|
|
16
18
|
*/
|
|
17
19
|
|
|
@@ -64,16 +66,35 @@ function isNode(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
64
66
|
return typeof value === "object" && value !== null;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function isBarePathsReference(node: unknown): boolean {
|
|
70
|
+
const reference = node as Readonly<Record<string, unknown>> | undefined;
|
|
71
|
+
return (
|
|
72
|
+
reference?.kind === "reference" &&
|
|
73
|
+
reference.name === PATHS_TYPE_NAME &&
|
|
74
|
+
(reference.typeArguments as readonly unknown[] | undefined)?.length === 0
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The `paths` root as a `$ref` or an `--enum-values` array indexes it: `paths`, or
|
|
80
|
+
* `FlattenedDeepRequired<paths>` (`rootType`), whose mapped keys are those of `paths`.
|
|
81
|
+
*/
|
|
82
|
+
function isPathsRoot(node: unknown): boolean {
|
|
83
|
+
const reference = node as Readonly<Record<string, unknown>> | undefined;
|
|
84
|
+
const typeArguments = reference?.typeArguments as readonly unknown[] | undefined;
|
|
85
|
+
const isDeepRoot =
|
|
86
|
+
reference?.kind === "reference" &&
|
|
87
|
+
reference.name === HELPER_TYPES.flattenedDeepRequired.name &&
|
|
88
|
+
typeArguments?.length === 1 &&
|
|
89
|
+
isBarePathsReference(typeArguments[0]);
|
|
90
|
+
return isDeepRoot || isBarePathsReference(node);
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
/** `paths["<url>"]`: an index of the `paths` root by a string literal. */
|
|
68
94
|
function pathsIndexUrl(node: Readonly<Record<string, unknown>>): string | undefined {
|
|
69
|
-
const object = node.object as Readonly<Record<string, unknown>> | undefined;
|
|
70
95
|
const index = node.index as Readonly<Record<string, unknown>> | undefined;
|
|
71
96
|
const isPathsIndex =
|
|
72
|
-
node.kind === "indexedAccess" &&
|
|
73
|
-
object?.kind === "reference" &&
|
|
74
|
-
object.name === PATHS_TYPE_NAME &&
|
|
75
|
-
(object.typeArguments as readonly unknown[] | undefined)?.length === 0 &&
|
|
76
|
-
index?.kind === "stringLiteral";
|
|
97
|
+
node.kind === "indexedAccess" && isPathsRoot(node.object) && index?.kind === "stringLiteral";
|
|
77
98
|
return isPathsIndex ? (index.value as string) : undefined;
|
|
78
99
|
}
|
|
79
100
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type LocatedNode,
|
|
9
9
|
} from "./located-node.js";
|
|
10
10
|
import {
|
|
11
|
+
hasKeptProperties,
|
|
11
12
|
isExcludedProperty,
|
|
12
13
|
isMarkedProperty,
|
|
13
14
|
isNullableSchema,
|
|
@@ -29,8 +30,16 @@ export type GeneratedMember =
|
|
|
29
30
|
/** `$Read<T>` or `$Write<T>` (`isMarkedProperty`): no member but the marker's own. */
|
|
30
31
|
readonly isMarked: boolean;
|
|
31
32
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* An index signature; `node` is undefined when its value type is `unknown`. `isOtherType`: the
|
|
35
|
+
* type read is not the type of the node the key names (`indexMember`).
|
|
36
|
+
*/
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: "index";
|
|
39
|
+
readonly node: unknown;
|
|
40
|
+
readonly location: string | undefined;
|
|
41
|
+
readonly isOtherType: boolean;
|
|
42
|
+
}
|
|
34
43
|
| { readonly kind: "missing" }
|
|
35
44
|
| { readonly kind: "opaque" };
|
|
36
45
|
|
|
@@ -91,19 +100,51 @@ function isBuiltInMember(key: string, prototype: object): boolean {
|
|
|
91
100
|
return key in prototype;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
/** `additionalProperties: true` or `{}` (also as a pattern's schema): a value of `unknown`. */
|
|
104
|
+
function isUnknownValue(value: unknown): boolean {
|
|
105
|
+
return value === true || (isJsonObject(value) && Object.keys(value).length === 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The value schemas of an index signature none of whose values is `unknown`, located. */
|
|
109
|
+
function typedIndexValues(owner: LocatedNode & { node: JsonObject }): LocatedNode[] {
|
|
95
110
|
const { additionalProperties: values, patternProperties: patterns } = owner.node;
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
const located: LocatedNode[] = [];
|
|
112
|
+
if (isJsonObject(values)) {
|
|
113
|
+
located.push({ node: values, location: childLocation(owner.location, "additionalProperties") });
|
|
98
114
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
for (const [pattern, node] of isJsonObject(patterns) ? Object.entries(patterns) : []) {
|
|
116
|
+
located.push({ node, location: childLocation(owner.location, "patternProperties", pattern) });
|
|
117
|
+
}
|
|
118
|
+
return located;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The index signature of an object schema, or null without one. Its type is the union of its
|
|
123
|
+
* value schemas' types (`unknown` if one is `unknown`); a union of several counts as opaque. It
|
|
124
|
+
* also holds the kept property types (`withPropertyTypes`), and a key that names a property
|
|
125
|
+
* `--exclude-deprecated` left out reads it instead of that property: both are `isOtherType`.
|
|
126
|
+
*/
|
|
127
|
+
function indexMember(
|
|
128
|
+
owner: LocatedNode & { node: JsonObject },
|
|
129
|
+
isExcludedKey: boolean,
|
|
130
|
+
input: MemberInput,
|
|
131
|
+
): GeneratedMember | null {
|
|
132
|
+
const { additionalProperties: values, patternProperties: patterns } = owner.node;
|
|
133
|
+
const patternValues = isJsonObject(patterns) ? Object.values(patterns) : [];
|
|
134
|
+
const isUnknown =
|
|
135
|
+
isUnknownValue(values) ||
|
|
136
|
+
patternValues.some(isUnknownValue) ||
|
|
137
|
+
(!values && input.options.additionalProperties);
|
|
138
|
+
if (isUnknown) {
|
|
139
|
+
return { kind: "index", node: undefined, location: undefined, isOtherType: isExcludedKey };
|
|
140
|
+
}
|
|
141
|
+
const typed = typedIndexValues(owner);
|
|
142
|
+
const [only] = typed;
|
|
143
|
+
if (only === undefined || typed.length > 1) {
|
|
144
|
+
return only === undefined ? null : OPAQUE;
|
|
102
145
|
}
|
|
103
|
-
const
|
|
104
|
-
return
|
|
105
|
-
? ({ kind: "index", node: undefined, location: undefined } satisfies GeneratedMember)
|
|
106
|
-
: null;
|
|
146
|
+
const isOtherType = isExcludedKey || hasKeptProperties(owner.node, input.options, input.refs);
|
|
147
|
+
return { kind: "index", ...only, isOtherType };
|
|
107
148
|
}
|
|
108
149
|
|
|
109
150
|
function objectMember(
|
|
@@ -114,7 +155,9 @@ function objectMember(
|
|
|
114
155
|
const { options } = input;
|
|
115
156
|
const { node } = owner;
|
|
116
157
|
const properties = isJsonObject(node.properties) ? node.properties : {};
|
|
117
|
-
|
|
158
|
+
const isExcludedKey =
|
|
159
|
+
Object.hasOwn(properties, key) && isExcludedProperty(properties[key], options, input.refs);
|
|
160
|
+
if (Object.hasOwn(properties, key) && !isExcludedKey) {
|
|
118
161
|
if (isJsonObject(node.discriminator)) {
|
|
119
162
|
return OPAQUE;
|
|
120
163
|
}
|
|
@@ -130,7 +173,7 @@ function objectMember(
|
|
|
130
173
|
return OPAQUE;
|
|
131
174
|
}
|
|
132
175
|
return hasObjectKeywords(node)
|
|
133
|
-
? (indexMember(owner,
|
|
176
|
+
? (indexMember(owner, isExcludedKey, input) ?? MISSING)
|
|
134
177
|
: emptyObjectMember(node, options);
|
|
135
178
|
}
|
|
136
179
|
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
/**
|
|
19
19
|
* Whether the type the original wrote for a `$ref` (`refType`: every segment indexed by name,
|
|
20
20
|
* `properties` skipped) surely fails to compile. It walks the document as TypeScript walks the
|
|
21
|
-
* generated types and answers true only where TypeScript reports an error for sure
|
|
22
|
-
*
|
|
21
|
+
* generated types and answers true only where TypeScript reports an error for sure, or where the
|
|
22
|
+
* type compiles but is read through an index signature that is not the target's type alone
|
|
23
|
+
* (`isOtherType`); where the answer depends on a shape it does not model, it answers false, so
|
|
24
|
+
* that output stays as it was.
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
27
|
type Step = "fails" | "compiles" | "continue";
|
|
@@ -42,7 +44,8 @@ function schemaStep(walk: OriginalWalk, segment: string, isLast: boolean, input:
|
|
|
42
44
|
return { step: "continue" as Step, walk };
|
|
43
45
|
}
|
|
44
46
|
const member = generatedMember(walk, segment, input.options, input.refs);
|
|
45
|
-
|
|
47
|
+
// Such an index signature compiles, but its type is not the target's alone.
|
|
48
|
+
if (member.kind === "missing" || (member.kind === "index" && member.isOtherType)) {
|
|
46
49
|
return { step: "fails" as Step, walk };
|
|
47
50
|
}
|
|
48
51
|
if (member.kind === "opaque") {
|
|
@@ -58,6 +58,15 @@ export function isExcludedProperty(
|
|
|
58
58
|
return options.excludeDeprecated && isJsonObject(resolved) && Boolean(resolved.deprecated);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Whether the generated type keeps one of the schema's properties. The index signature next to
|
|
63
|
+
* them then holds their types too (`withPropertyTypes`), so it is not its value schema's type.
|
|
64
|
+
*/
|
|
65
|
+
export function hasKeptProperties(node: JsonObject, options: SchemaOptions, refs: RefResolver) {
|
|
66
|
+
const properties = isJsonObject(node.properties) ? node.properties : {};
|
|
67
|
+
return Object.values(properties).some((value) => !isExcludedProperty(value, options, refs));
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Whether `--read-write-markers` wraps the type of a property or `$defs` member: one that is only
|
|
63
72
|
* `readOnly` or only `writeOnly` is `$Read<T>` or `$Write<T>` (object-type.ts).
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
startsInSchema,
|
|
25
25
|
} from "./pointer-walk.js";
|
|
26
26
|
import {
|
|
27
|
+
hasKeptProperties,
|
|
27
28
|
isExcludedProperty,
|
|
28
29
|
isMarkedProperty,
|
|
29
30
|
type Presence,
|
|
@@ -34,9 +35,10 @@ import {
|
|
|
34
35
|
/**
|
|
35
36
|
* The type of a `$ref` pointer as the generated types have it: the `schema` of a media type,
|
|
36
37
|
* header or component parameter is its type, `items` is indexed by `number`,
|
|
37
|
-
* `additionalProperties` by `string
|
|
38
|
-
* `NonNullable` before it is indexed. The walk
|
|
39
|
-
* (`allOf/0`, `definitions/A`, operation
|
|
38
|
+
* `additionalProperties` by `string` where the index signature is its type alone, and a value
|
|
39
|
+
* that may be `undefined` or `null` is wrapped in `NonNullable` before it is indexed. The walk
|
|
40
|
+
* stops at a segment with no type of its own (`allOf/0`, `definitions/A`, operation
|
|
41
|
+
* `parameters/0`): the caller types the target itself.
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
44
|
const DEFS_KEYWORD = "$defs";
|
|
@@ -133,6 +135,8 @@ function defsStep(walk: TypedWalk, owner: LocatedNode, isLast: boolean): TypedWa
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
const COMPOSITION_KEYWORDS = ["allOf", "anyOf", "oneOf"] as const;
|
|
139
|
+
|
|
136
140
|
const SHAPED_KEYWORDS: ReadonlySet<string> = new Set([
|
|
137
141
|
"properties",
|
|
138
142
|
"items",
|
|
@@ -151,6 +155,21 @@ function hasKeywordMember(owner: unknown, segment: string): boolean {
|
|
|
151
155
|
return segment === "items" ? isArrayShape(owner) : isObjectShape(owner);
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether `[string]` of the owner's type is its `additionalProperties` type. Not where the index
|
|
160
|
+
* signature also holds pattern or property types (`withPropertyTypes`), nor next to `allOf` /
|
|
161
|
+
* `anyOf` / `oneOf`, whose members may add an index signature or an inherited discriminator.
|
|
162
|
+
*/
|
|
163
|
+
function isIndexedByValue(owner: unknown, input: TypedWalkInput): boolean {
|
|
164
|
+
if (!isJsonObject(owner)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const patterns = owner.patternProperties;
|
|
168
|
+
const hasPatterns = isJsonObject(patterns) && Object.keys(patterns).length > 0;
|
|
169
|
+
const isComposed = COMPOSITION_KEYWORDS.some((keyword) => keyword in owner);
|
|
170
|
+
return !hasPatterns && !isComposed && !hasKeptProperties(owner, input.options, input.refs);
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
function keywordStep(walk: TypedWalk, segment: string, isLast: boolean, input: TypedWalkInput) {
|
|
155
174
|
const owner = followLocated(walk, input.refs);
|
|
156
175
|
if (!hasKeywordMember(owner.node, segment)) {
|
|
@@ -162,7 +181,10 @@ function keywordStep(walk: TypedWalk, segment: string, isLast: boolean, input: T
|
|
|
162
181
|
case "items":
|
|
163
182
|
return indexEntries(walk, "number", "items", input.refs);
|
|
164
183
|
case "additionalProperties":
|
|
165
|
-
|
|
184
|
+
// Otherwise the walk stops, and the target is typed in place.
|
|
185
|
+
return isIndexedByValue(owner.node, input)
|
|
186
|
+
? indexEntries(walk, "string", "additionalProperties", input.refs)
|
|
187
|
+
: null;
|
|
166
188
|
case DEFS_KEYWORD:
|
|
167
189
|
return defsStep(walk, owner, isLast);
|
|
168
190
|
default:
|
package/src/cli/api-writes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClaimedFile } from "../application/ports/file-system.js";
|
|
2
2
|
import type { GeneratedTypes } from "./cli-dependencies.js";
|
|
3
|
+
import { ApiFailure } from "./cli-errors.js";
|
|
3
4
|
import {
|
|
4
5
|
type CliRun,
|
|
5
6
|
duplicateOutputError,
|
|
@@ -84,10 +85,17 @@ function writeClaimed(run: CliRun, { api, claim }: ClaimedApi): unknown[] {
|
|
|
84
85
|
: failures;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** The failures with the name of their API, which the CLI prints before each message. */
|
|
89
|
+
function namedFailures(failures: ApiFailures): ApiFailures {
|
|
90
|
+
return new Map(
|
|
91
|
+
[...failures].map(([name, list]) => [name, list.map((cause) => new ApiFailure(name, cause))]),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
/**
|
|
88
96
|
* Write (or `--check`) every API unless its output is a file the run read or the file of another
|
|
89
|
-
* API. Returns the failures by API name
|
|
90
|
-
* one claim created is empty by the time it is removed.
|
|
97
|
+
* API. Returns the failures by API name, each an `ApiFailure`. Claims are released in reverse
|
|
98
|
+
* order, so a directory one claim created is empty by the time it is removed.
|
|
91
99
|
*/
|
|
92
100
|
export function writeGeneratedApis(
|
|
93
101
|
run: CliRun,
|
|
@@ -113,5 +121,5 @@ export function writeGeneratedApis(
|
|
|
113
121
|
for (const entry of claimed.filter(({ api }) => !shared.has(api.name))) {
|
|
114
122
|
failures.set(entry.api.name, writeClaimed(run, entry));
|
|
115
123
|
}
|
|
116
|
-
return failures;
|
|
124
|
+
return namedFailures(failures);
|
|
117
125
|
}
|