@maroonedog/luq-codegen 0.1.0
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/LICENSE +9 -0
- package/README.md +258 -0
- package/dist/generate/chain-call.types.d.ts +40 -0
- package/dist/generate/chain-call.types.js +10 -0
- package/dist/generate/chain-call.types.mjs +9 -0
- package/dist/generate/generate-validator-module.d.ts +11 -0
- package/dist/generate/generate-validator-module.js +130 -0
- package/dist/generate/generate-validator-module.mjs +127 -0
- package/dist/generate/keyword-to-chain-call.d.ts +10 -0
- package/dist/generate/keyword-to-chain-call.js +111 -0
- package/dist/generate/keyword-to-chain-call.mjs +107 -0
- package/dist/generate/resolve-required-path.d.ts +18 -0
- package/dist/generate/resolve-required-path.js +98 -0
- package/dist/generate/resolve-required-path.mjs +93 -0
- package/dist/generate/slot-for-schema.d.ts +13 -0
- package/dist/generate/slot-for-schema.js +28 -0
- package/dist/generate/slot-for-schema.mjs +25 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/index.mjs +8 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Luq Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# @maroonedog/luq-codegen
|
|
2
|
+
|
|
3
|
+
Turns a JSON Schema Draft-07 schema **object** into the **source** of a
|
|
4
|
+
TypeScript module that exports a Luq validator. An OpenAPI 3.0 component schema
|
|
5
|
+
is that shape, so a caller that has already parsed the document can hand one
|
|
6
|
+
straight in.
|
|
7
|
+
|
|
8
|
+
It writes source and returns it as a string. It does not read a file, parse
|
|
9
|
+
YAML, walk `paths` or `components`, or write anything to disk — the caller does
|
|
10
|
+
all of that. See [the worked example](#a-worked-example).
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
- **Not published.** `npm install @maroonedog/luq-codegen` does not resolve.
|
|
15
|
+
The only way to use it today is from this checkout, with a `file:` specifier —
|
|
16
|
+
which is what [`examples/openapi`](../examples/openapi) does.
|
|
17
|
+
- **It is not a plugin for `@hey-api/openapi-ts`.** The name says otherwise;
|
|
18
|
+
nothing in `src/` refers to that project, and there is no `defineConfig` or
|
|
19
|
+
plugin handler here. The name is unfinished business, not a feature.
|
|
20
|
+
- **There is no CLI.** One exported function, called from your own script.
|
|
21
|
+
- It imports `@maroonedog/luq/schema-tooling` — the library's own schema
|
|
22
|
+
flattener — so that the paths it writes and the paths the run-time converter
|
|
23
|
+
produces come from the same code. `peerDependencies` therefore names
|
|
24
|
+
`@maroonedog/luq` `>=2.4.0`: no earlier release carries that subpath. If the
|
|
25
|
+
subpath ships under a different number, that range moves with it.
|
|
26
|
+
|
|
27
|
+
## The API
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { generateValidatorModule } from "@maroonedog/luq-codegen";
|
|
31
|
+
import type { GenerateOptions, GeneratedModule } from "@maroonedog/luq-codegen";
|
|
32
|
+
|
|
33
|
+
generateValidatorModule(schema: Draft07Schema, options: GenerateOptions): GeneratedModule;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`schema` is the Draft-07 document, already an object in memory.
|
|
37
|
+
|
|
38
|
+
### `GenerateOptions`
|
|
39
|
+
|
|
40
|
+
| Field | | What it is |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `validatorName` | required | The name of the const to declare, e.g. `"orderValidator"`. |
|
|
43
|
+
| `typeExpression` | required | Emitted verbatim inside `.for<…>()`, e.g. `'components["schemas"]["Order"]'`. |
|
|
44
|
+
| `typeImport` | optional | One block of source emitted after `import { Builder }`. Omitted, the type named by `typeExpression` is assumed to be in scope already. |
|
|
45
|
+
|
|
46
|
+
### `GeneratedModule`
|
|
47
|
+
|
|
48
|
+
| Field | What it is |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `source` | The whole module text: the header comment, the skipped-keyword notice, the imports, the builder chain, a trailing newline. |
|
|
51
|
+
| `pluginExports` | The plugin export names used, deduplicated and sorted. Every one of them is both imported and `.use()`d in `source`, and nothing else is. |
|
|
52
|
+
| `skipped` | `{ path, keyword, reason }` for every keyword that produced no rule. `path` is `""` for the root. |
|
|
53
|
+
|
|
54
|
+
`ChainCall`, `FieldChain` and `SkippedKeyword` are exported as types as well.
|
|
55
|
+
|
|
56
|
+
Two things are normalised, and both are pinned by tests: the keywords within one
|
|
57
|
+
field are walked in sorted key order, and the plugin imports are sorted by export
|
|
58
|
+
name. So the same schema object always produces the same source.
|
|
59
|
+
|
|
60
|
+
**Field order is not normalised.** It follows the document's `properties` order,
|
|
61
|
+
so writing the same two properties the other way round produces the same rules in
|
|
62
|
+
the other order. If you diff generated output across regenerations, reorder a
|
|
63
|
+
property upstream and the diff will show it.
|
|
64
|
+
|
|
65
|
+
## A worked example
|
|
66
|
+
|
|
67
|
+
[`examples/openapi`](../examples/openapi) is a running one — one
|
|
68
|
+
`openapi.yaml`, types from `openapi-typescript`, rules from this generator,
|
|
69
|
+
`npm run verify` to generate, typecheck and run it. Its README explains the
|
|
70
|
+
setup; the part that concerns this package is the call:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const document = parse(readFileSync(SPEC, "utf8"));
|
|
74
|
+
const schema = document.components.schemas.Order;
|
|
75
|
+
|
|
76
|
+
const { source, skipped } = generateValidatorModule(schema, {
|
|
77
|
+
validatorName: "orderValidator",
|
|
78
|
+
typeExpression: 'components["schemas"]["Order"]',
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The script picks the component out of the parsed document, prepends its own
|
|
83
|
+
`import type { components } from "./api.generated";` line, and writes `source`
|
|
84
|
+
to a file. What comes out is ordinary Luq:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export const orderValidator = Builder()
|
|
88
|
+
.use(arrayMinLengthPlugin)
|
|
89
|
+
.use(numberMinPlugin)
|
|
90
|
+
// …
|
|
91
|
+
.for<components["schemas"]["Order"]>()
|
|
92
|
+
.v("id", (b) => b.string.required().uuid())
|
|
93
|
+
.v("customer.email", (b) => b.string.required().email())
|
|
94
|
+
.v("lines", (b) => b.array.required().minLength(1))
|
|
95
|
+
.v("lines[*].quantity", (b) => b.number.required().min(1))
|
|
96
|
+
.build();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What a keyword becomes
|
|
100
|
+
|
|
101
|
+
| Keyword | Method | | Keyword | Method |
|
|
102
|
+
|---|---|---|---|---|
|
|
103
|
+
| `minLength` | `.min()` | | `maxItems` | `.maxLength()` |
|
|
104
|
+
| `maxLength` | `.max()` | | `uniqueItems: true` | `.unique()` |
|
|
105
|
+
| `pattern` | `.pattern()` | | `minProperties` | `.minProperties()` |
|
|
106
|
+
| `minimum` | `.min()` | | `maxProperties` | `.maxProperties()` |
|
|
107
|
+
| `maximum` | `.max()` | | `const` | `.literal()` |
|
|
108
|
+
| `multipleOf` | `.multipleOf()` | | `enum` | `.oneOf()` |
|
|
109
|
+
| `minItems` | `.minLength()` | | | |
|
|
110
|
+
|
|
111
|
+
`format` values that become a method: `email`, `uuid`, `uri` (`.url()`),
|
|
112
|
+
`hostname`, `ipv4`, `ipv6`, `date`, `date-time` (`.datetime()`), `time`,
|
|
113
|
+
`duration`, `json-pointer` (`.jsonPointer()`), `iri`. Every other `format` is
|
|
114
|
+
skipped by name.
|
|
115
|
+
|
|
116
|
+
`type` chooses the slot after `b.`: `string`, `number` (also for `integer`),
|
|
117
|
+
`boolean`, `array`, `object`. Anything else — no `type`, several types, `null`
|
|
118
|
+
alone — falls to `any`, which checks nothing.
|
|
119
|
+
|
|
120
|
+
Only the plugins actually used are imported, each from its own subpath
|
|
121
|
+
(`@maroonedog/luq/plugins/uuid`, never the barrel), so the generated module
|
|
122
|
+
pays for what it declares and nothing more.
|
|
123
|
+
|
|
124
|
+
## Nothing is dropped in silence
|
|
125
|
+
|
|
126
|
+
A keyword that cannot become a rule is returned in `skipped` **and** named in a
|
|
127
|
+
comment at the top of the generated file:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
// Keywords in this schema that did not become rules:
|
|
131
|
+
// id: type — selects the slot; it is not a method
|
|
132
|
+
// customer: properties — expands into declarations for the child fields
|
|
133
|
+
// lines: items — expands into the declaration for array elements
|
|
134
|
+
// … one line per keyword; the real block for examples/openapi has twelve
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Most entries are structural rather than omissions: `type` chooses the slot, and
|
|
138
|
+
`properties`, `items` and `required` become declarations rather than rules. The
|
|
139
|
+
ones worth reading are the rest — a keyword with **no chain method** is a
|
|
140
|
+
constraint the document states and the generated validator does not check.
|
|
141
|
+
|
|
142
|
+
Three different things share that list, and a reader has to separate them:
|
|
143
|
+
|
|
144
|
+
- **Structural** — `type`, `properties`, `items`, `required`, `$ref`. Nothing
|
|
145
|
+
was lost; these decide the slot and the shape of the declarations instead of
|
|
146
|
+
becoming rules of their own.
|
|
147
|
+
- **Deliberate** — annotations (`title`, `description`, `default`, `example`,
|
|
148
|
+
`deprecated`, `readOnly`, `writeOnly`), and `uniqueItems: false`, which in
|
|
149
|
+
Draft-07 is the absence of a constraint.
|
|
150
|
+
- **Unexpressed** — the reason reads `no chain method corresponds to it` or
|
|
151
|
+
`no plugin corresponds to format "…"`. This is the set to read before
|
|
152
|
+
trusting the output: the document says something the validator does not
|
|
153
|
+
check.
|
|
154
|
+
|
|
155
|
+
`examples/openapi/scripts/generate.mjs` filters the first group out and prints
|
|
156
|
+
what is left, which is the shape to copy in your own script.
|
|
157
|
+
|
|
158
|
+
## What it does not handle
|
|
159
|
+
|
|
160
|
+
A validator that quietly checks less than the document says is the failure
|
|
161
|
+
worth naming, so here is everything known to be missing.
|
|
162
|
+
|
|
163
|
+
**Keywords that never become a rule.** `exclusiveMinimum`, `exclusiveMaximum`,
|
|
164
|
+
`oneOf`, `anyOf`, `not`, `if`/`then`/`else`, `contains`, `dependencies`,
|
|
165
|
+
`propertyNames`, `patternProperties`, `additionalProperties`,
|
|
166
|
+
`additionalItems`, `contentEncoding`, `contentMediaType`. Each is reported in
|
|
167
|
+
`skipped`; none is checked.
|
|
168
|
+
|
|
169
|
+
**`format` values outside the twelve above** — `byte`, `int64`,
|
|
170
|
+
`uri-reference`, `regex`, `idn-email` and the rest. Reported by name, not
|
|
171
|
+
checked.
|
|
172
|
+
|
|
173
|
+
**A child-level `allOf` is dropped whole.** Only an `allOf` at the root is
|
|
174
|
+
folded into the schema before flattening. On a nested schema, both its
|
|
175
|
+
constraints *and* the properties it contributes disappear: the field gets a
|
|
176
|
+
bare `b.any.optional()` and no declarations for the subtree. The keyword is
|
|
177
|
+
listed in `skipped`, but its reason text says "folded away during flattening",
|
|
178
|
+
which is true only at the root.
|
|
179
|
+
|
|
180
|
+
**Tuple `items: [A, B]`** produces the array rule and no element declarations.
|
|
181
|
+
|
|
182
|
+
**`nullable: true` produces no rule.** On a required field the emitted
|
|
183
|
+
`.required()` rejects `null`, so the generated validator is stricter than the
|
|
184
|
+
document.
|
|
185
|
+
|
|
186
|
+
**`$ref` resolves only inside the object you pass.** A component schema
|
|
187
|
+
containing `$ref: "#/components/schemas/Line"` makes the function **throw**,
|
|
188
|
+
because there is no `components` under the root it was given. Passing the whole
|
|
189
|
+
document as the root does resolve it, at the cost of a root-level declaration —
|
|
190
|
+
see below. `#/definitions/…` inside a self-contained schema works.
|
|
191
|
+
|
|
192
|
+
**A constraint on a slotless field emits a method that slot does not have.**
|
|
193
|
+
`{ "a": { "minLength": 2 } }` — no `type` — becomes
|
|
194
|
+
`.v("a", (b) => b.any.optional().min(2))`, and `min` is not on the `any` slot.
|
|
195
|
+
The same happens for `enum` anywhere but a `string`, `number` or `boolean`
|
|
196
|
+
field, and for any rule-bearing keyword at the **root** — an array-rooted
|
|
197
|
+
schema with `minItems`, for instance, or a whole OpenAPI document handed in as
|
|
198
|
+
the root — which becomes `.v("", (b) => b.any.…)`.
|
|
199
|
+
|
|
200
|
+
This fails loudly rather than silently: the method is not in the slot's type,
|
|
201
|
+
so the generated file does not compile.
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
error TS2339: Property 'min' does not exist on type 'FieldChain<…, "any", …>'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
That is still a broken generation. Give the field a `type` in the document, or
|
|
208
|
+
write that rule by hand.
|
|
209
|
+
|
|
210
|
+
**A nested `required` is lowered only when every ancestor object is itself
|
|
211
|
+
required.** In Draft-07 a subschema applies only to a value that exists, so
|
|
212
|
+
`{ properties: { a: { required: ["b"] } } }` accepts `{}`. Emitting
|
|
213
|
+
`.required()` on `"a.b"` would reject it and disagree with the run-time
|
|
214
|
+
converter. When an ancestor is not required the generator emits `.optional()`
|
|
215
|
+
instead and reports the omission in `skipped` with that reason. An array in the
|
|
216
|
+
path does not break the chain: an absent array has no elements, so no element
|
|
217
|
+
rule runs.
|
|
218
|
+
|
|
219
|
+
## Generating, or converting at run time
|
|
220
|
+
|
|
221
|
+
This package writes source you commit and read. The library can also convert a
|
|
222
|
+
Draft-07 document at run time, with `fromJsonSchema` — that path handles the
|
|
223
|
+
whole of Draft-07 rather than the subset above, and is the right answer when
|
|
224
|
+
the document is only known at run time. See
|
|
225
|
+
[docs/guide/json-schema.md](../docs/guide/json-schema.md).
|
|
226
|
+
|
|
227
|
+
Generating is the right answer when the document is known now: the rules are
|
|
228
|
+
plain code you can diff and step through, and they are declared against the
|
|
229
|
+
generated type, so a spec change that renames a field breaks the compiler
|
|
230
|
+
instead of the validator.
|
|
231
|
+
|
|
232
|
+
## Working on it
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm install --ignore-scripts # links @maroonedog/luq from the repository root
|
|
236
|
+
npm run verify # typecheck, build, dist gates, tests
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`npm run build` emits `dist/` with the same shape as the library: `.js` is
|
|
240
|
+
CommonJS, `.mjs` is ESM, `.d.ts` beside them, one emitted module per source
|
|
241
|
+
module, nothing bundled. It reuses the root's own emit steps rather than
|
|
242
|
+
running a second build system.
|
|
243
|
+
|
|
244
|
+
The library must be built first (`npm run build` at the repository root),
|
|
245
|
+
because this package imports it by package specifier and resolves it through
|
|
246
|
+
its `dist`. CI runs both in that order.
|
|
247
|
+
|
|
248
|
+
`check:dist` runs the same two gates as the root package: no dynamic code in
|
|
249
|
+
anything shipped, and no private artefact or unresolvable **relative**
|
|
250
|
+
specifier in `dist`. It does not resolve bare ones, so the import of
|
|
251
|
+
`@maroonedog/luq/schema-tooling` that every emitted module carries is not its
|
|
252
|
+
business — that is covered by loading the built entries for real, which
|
|
253
|
+
`test/built-package.test.ts` does under both `require` and ESM.
|
|
254
|
+
|
|
255
|
+
The tests cover the generator, the keyword table against the library's, the
|
|
256
|
+
built CommonJS and ESM entries, and one end-to-end case that writes a generated
|
|
257
|
+
module to a temp directory, compiles it with `tsc --strict`, and runs it
|
|
258
|
+
against a good and a bad document.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One call of one method on the chain. */
|
|
2
|
+
export interface ChainCall {
|
|
3
|
+
/** The method to call, e.g. "min". */
|
|
4
|
+
readonly method: string;
|
|
5
|
+
/** The argument source, emitted verbatim, e.g. ["3"]. Empty gives `.min()`. */
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* The export name of the plugin carrying this method, e.g.
|
|
9
|
+
* "stringMinPlugin". The import statements are built by collecting these.
|
|
10
|
+
*/
|
|
11
|
+
readonly pluginExport: string;
|
|
12
|
+
/** The subpath to import it from, e.g. "@maroonedog/luq/plugins/stringMin". */
|
|
13
|
+
readonly pluginSubpath: string;
|
|
14
|
+
}
|
|
15
|
+
/** One field's declaration, becoming `.v(path, b => b.<slot>....)`. */
|
|
16
|
+
export interface FieldChain {
|
|
17
|
+
/** The first argument of `.v()`, e.g. "items[*].sku". */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
/** The slot name following `b.`, e.g. "string". */
|
|
20
|
+
readonly slot: string;
|
|
21
|
+
readonly calls: readonly ChainCall[];
|
|
22
|
+
/**
|
|
23
|
+
* The keywords not emitted, with reasons. Carried through so that dropping
|
|
24
|
+
* one is never silent, and printed in a comment in the output.
|
|
25
|
+
*/
|
|
26
|
+
readonly skipped: readonly SkippedKeyword[];
|
|
27
|
+
}
|
|
28
|
+
export interface SkippedKeyword {
|
|
29
|
+
readonly keyword: string;
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GeneratedModule {
|
|
33
|
+
readonly source: string;
|
|
34
|
+
/** The export names of the plugins used, deduplicated and sorted. */
|
|
35
|
+
readonly pluginExports: readonly string[];
|
|
36
|
+
/** Everything skipped, across all fields, for the caller to report. */
|
|
37
|
+
readonly skipped: readonly (SkippedKeyword & {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
})[];
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ===========================================================================
|
|
3
|
+
// luq-codegen/src/generate/chain-call.types.ts
|
|
4
|
+
//
|
|
5
|
+
// The intermediate form of one generated chain. Going through it instead of
|
|
6
|
+
// concatenating strings is what makes the set of plugins to import countable
|
|
7
|
+
// afterwards. Built by concatenation, a missing import first shows up in the
|
|
8
|
+
// user's compiler rather than here.
|
|
9
|
+
// ===========================================================================
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// luq-codegen/src/generate/chain-call.types.ts
|
|
3
|
+
//
|
|
4
|
+
// The intermediate form of one generated chain. Going through it instead of
|
|
5
|
+
// concatenating strings is what makes the set of plugins to import countable
|
|
6
|
+
// afterwards. Built by concatenation, a missing import first shows up in the
|
|
7
|
+
// user's compiler rather than here.
|
|
8
|
+
// ===========================================================================
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Draft07Schema } from "@maroonedog/luq/schema-tooling";
|
|
2
|
+
import type { GeneratedModule } from "./chain-call.types";
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
/** The name of the const to generate, e.g. "validateOrder". */
|
|
5
|
+
readonly validatorName: string;
|
|
6
|
+
/** The type name to put in `.for<T>()`, e.g. 'components["schemas"]["Order"]'. */
|
|
7
|
+
readonly typeExpression: string;
|
|
8
|
+
/** The line importing that type. Omitted, the type is assumed in scope. */
|
|
9
|
+
readonly typeImport?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateValidatorModule(schema: Draft07Schema, options: GenerateOptions): GeneratedModule;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateValidatorModule = generateValidatorModule;
|
|
4
|
+
// ===========================================================================
|
|
5
|
+
// luq-codegen/src/generate/generate-validator-module.ts
|
|
6
|
+
//
|
|
7
|
+
// One schema to the source of a module exporting one validator.
|
|
8
|
+
//
|
|
9
|
+
// Flattening reuses the library's own. The generated code and the run-time
|
|
10
|
+
// conversion decide paths with the same function, which makes it structurally
|
|
11
|
+
// impossible for the two to disagree about what a path looks like.
|
|
12
|
+
// ===========================================================================
|
|
13
|
+
const schema_tooling_1 = require("@maroonedog/luq/schema-tooling");
|
|
14
|
+
const keyword_to_chain_call_1 = require("./keyword-to-chain-call");
|
|
15
|
+
const resolve_required_path_1 = require("./resolve-required-path");
|
|
16
|
+
const slot_for_schema_1 = require("./slot-for-schema");
|
|
17
|
+
const PRESENCE_REQUIRED = {
|
|
18
|
+
method: "required",
|
|
19
|
+
args: [],
|
|
20
|
+
pluginExport: "requiredPlugin",
|
|
21
|
+
pluginSubpath: "@maroonedog/luq/plugins/required",
|
|
22
|
+
};
|
|
23
|
+
const PRESENCE_OPTIONAL = {
|
|
24
|
+
method: "optional",
|
|
25
|
+
args: [],
|
|
26
|
+
pluginExport: "optionalPlugin",
|
|
27
|
+
pluginSubpath: "@maroonedog/luq/plugins/optional",
|
|
28
|
+
};
|
|
29
|
+
function toFieldChain(path, schema, isRequired) {
|
|
30
|
+
const calls = [isRequired ? PRESENCE_REQUIRED : PRESENCE_OPTIONAL];
|
|
31
|
+
const skipped = [];
|
|
32
|
+
// Walked in key order rather than order of appearance. Letting how the
|
|
33
|
+
// schema was written decide the chain order fills every diff with noise.
|
|
34
|
+
for (const keyword of Object.keys(schema).sort()) {
|
|
35
|
+
const outcome = (0, keyword_to_chain_call_1.keywordToChainCall)(keyword, schema[keyword]);
|
|
36
|
+
if (outcome.call !== undefined)
|
|
37
|
+
calls.push(outcome.call);
|
|
38
|
+
if (outcome.skipped !== undefined)
|
|
39
|
+
skipped.push(outcome.skipped);
|
|
40
|
+
}
|
|
41
|
+
return { path, slot: (0, slot_for_schema_1.slotForSchema)(schema), calls, skipped };
|
|
42
|
+
}
|
|
43
|
+
function renderChain(field) {
|
|
44
|
+
const body = field.calls
|
|
45
|
+
.map((call) => `.${call.method}(${call.args.join(", ")})`)
|
|
46
|
+
.join("");
|
|
47
|
+
return ` .v(${JSON.stringify(field.path)}, (b) => b.${field.slot}${body})`;
|
|
48
|
+
}
|
|
49
|
+
function renderImports(pluginExports, typeImport) {
|
|
50
|
+
const lines = ['import { Builder } from "@maroonedog/luq";'];
|
|
51
|
+
if (typeImport !== undefined)
|
|
52
|
+
lines.push(typeImport);
|
|
53
|
+
for (const entry of pluginExports) {
|
|
54
|
+
lines.push(`import { ${entry.name} } from ${JSON.stringify(entry.subpath)};`);
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Skipped keywords are listed in a comment at the top of the output, so a
|
|
60
|
+
* reader can see what the generator dropped without leaving the file.
|
|
61
|
+
*/
|
|
62
|
+
function renderSkippedNotice(skipped) {
|
|
63
|
+
if (skipped.length === 0)
|
|
64
|
+
return "";
|
|
65
|
+
const lines = skipped.map((entry) => `// ${entry.path || "(root)"}: ${entry.keyword} — ${entry.reason}`);
|
|
66
|
+
return [
|
|
67
|
+
"//",
|
|
68
|
+
"// Keywords in this schema that did not become rules:",
|
|
69
|
+
...lines,
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
function generateValidatorModule(schema, options) {
|
|
74
|
+
const declarations = (0, schema_tooling_1.flattenSchema)(schema, schema_tooling_1.readChildSchemas);
|
|
75
|
+
const fields = declarations.map((declaration) => {
|
|
76
|
+
// Flattening reports isRequired for the root's own keys only. A nested
|
|
77
|
+
// required is a rule on the object at run time, and the chain has nowhere
|
|
78
|
+
// to put that. See resolve-required-path.ts for when it can be lowered.
|
|
79
|
+
const safelyRequired = declaration.isRequired || (0, resolve_required_path_1.isSafelyRequired)(schema, declaration.path);
|
|
80
|
+
const field = toFieldChain(declaration.path, declaration.schema, safelyRequired);
|
|
81
|
+
if (safelyRequired || !(0, resolve_required_path_1.isListedByParent)(schema, declaration.path)) {
|
|
82
|
+
return field;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...field,
|
|
86
|
+
skipped: [
|
|
87
|
+
...field.skipped,
|
|
88
|
+
{
|
|
89
|
+
keyword: "required",
|
|
90
|
+
reason: "the parent schema marks it required, but an ancestor object is " +
|
|
91
|
+
"not, so .required() would wrongly reject a document missing that " +
|
|
92
|
+
"ancestor; Draft-07 applies a subschema only to a value that " +
|
|
93
|
+
"exists, so this was emitted as optional",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const byExport = new Map();
|
|
99
|
+
for (const field of fields) {
|
|
100
|
+
for (const call of field.calls)
|
|
101
|
+
byExport.set(call.pluginExport, call.pluginSubpath);
|
|
102
|
+
}
|
|
103
|
+
const pluginExports = [...byExport.entries()]
|
|
104
|
+
.map(([name, subpath]) => ({ name, subpath }))
|
|
105
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
106
|
+
const skipped = fields.flatMap((field) => field.skipped.map((entry) => ({ ...entry, path: field.path })));
|
|
107
|
+
const uses = pluginExports.map((entry) => ` .use(${entry.name})`).join("\n");
|
|
108
|
+
const chains = fields.map(renderChain).join("\n");
|
|
109
|
+
const source = [
|
|
110
|
+
"// Generated by @maroonedog/luq-codegen. Do not edit.",
|
|
111
|
+
"// Rules are declared against the generated type, so a spec change that",
|
|
112
|
+
"// renames a field makes this file stop compiling instead of drifting.",
|
|
113
|
+
renderSkippedNotice(skipped),
|
|
114
|
+
renderImports(pluginExports, options.typeImport),
|
|
115
|
+
"",
|
|
116
|
+
`export const ${options.validatorName} = Builder()`,
|
|
117
|
+
uses,
|
|
118
|
+
` .for<${options.typeExpression}>()`,
|
|
119
|
+
chains,
|
|
120
|
+
" .build();",
|
|
121
|
+
"",
|
|
122
|
+
]
|
|
123
|
+
.filter((part) => part !== "")
|
|
124
|
+
.join("\n");
|
|
125
|
+
return {
|
|
126
|
+
source,
|
|
127
|
+
pluginExports: pluginExports.map((entry) => entry.name),
|
|
128
|
+
skipped,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// luq-codegen/src/generate/generate-validator-module.ts
|
|
3
|
+
//
|
|
4
|
+
// One schema to the source of a module exporting one validator.
|
|
5
|
+
//
|
|
6
|
+
// Flattening reuses the library's own. The generated code and the run-time
|
|
7
|
+
// conversion decide paths with the same function, which makes it structurally
|
|
8
|
+
// impossible for the two to disagree about what a path looks like.
|
|
9
|
+
// ===========================================================================
|
|
10
|
+
import { flattenSchema, readChildSchemas } from "@maroonedog/luq/schema-tooling";
|
|
11
|
+
import { keywordToChainCall } from "./keyword-to-chain-call.mjs";
|
|
12
|
+
import { isListedByParent, isSafelyRequired } from "./resolve-required-path.mjs";
|
|
13
|
+
import { slotForSchema } from "./slot-for-schema.mjs";
|
|
14
|
+
const PRESENCE_REQUIRED = {
|
|
15
|
+
method: "required",
|
|
16
|
+
args: [],
|
|
17
|
+
pluginExport: "requiredPlugin",
|
|
18
|
+
pluginSubpath: "@maroonedog/luq/plugins/required",
|
|
19
|
+
};
|
|
20
|
+
const PRESENCE_OPTIONAL = {
|
|
21
|
+
method: "optional",
|
|
22
|
+
args: [],
|
|
23
|
+
pluginExport: "optionalPlugin",
|
|
24
|
+
pluginSubpath: "@maroonedog/luq/plugins/optional",
|
|
25
|
+
};
|
|
26
|
+
function toFieldChain(path, schema, isRequired) {
|
|
27
|
+
const calls = [isRequired ? PRESENCE_REQUIRED : PRESENCE_OPTIONAL];
|
|
28
|
+
const skipped = [];
|
|
29
|
+
// Walked in key order rather than order of appearance. Letting how the
|
|
30
|
+
// schema was written decide the chain order fills every diff with noise.
|
|
31
|
+
for (const keyword of Object.keys(schema).sort()) {
|
|
32
|
+
const outcome = keywordToChainCall(keyword, schema[keyword]);
|
|
33
|
+
if (outcome.call !== undefined)
|
|
34
|
+
calls.push(outcome.call);
|
|
35
|
+
if (outcome.skipped !== undefined)
|
|
36
|
+
skipped.push(outcome.skipped);
|
|
37
|
+
}
|
|
38
|
+
return { path, slot: slotForSchema(schema), calls, skipped };
|
|
39
|
+
}
|
|
40
|
+
function renderChain(field) {
|
|
41
|
+
const body = field.calls
|
|
42
|
+
.map((call) => `.${call.method}(${call.args.join(", ")})`)
|
|
43
|
+
.join("");
|
|
44
|
+
return ` .v(${JSON.stringify(field.path)}, (b) => b.${field.slot}${body})`;
|
|
45
|
+
}
|
|
46
|
+
function renderImports(pluginExports, typeImport) {
|
|
47
|
+
const lines = ['import { Builder } from "@maroonedog/luq";'];
|
|
48
|
+
if (typeImport !== undefined)
|
|
49
|
+
lines.push(typeImport);
|
|
50
|
+
for (const entry of pluginExports) {
|
|
51
|
+
lines.push(`import { ${entry.name} } from ${JSON.stringify(entry.subpath)};`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Skipped keywords are listed in a comment at the top of the output, so a
|
|
57
|
+
* reader can see what the generator dropped without leaving the file.
|
|
58
|
+
*/
|
|
59
|
+
function renderSkippedNotice(skipped) {
|
|
60
|
+
if (skipped.length === 0)
|
|
61
|
+
return "";
|
|
62
|
+
const lines = skipped.map((entry) => `// ${entry.path || "(root)"}: ${entry.keyword} — ${entry.reason}`);
|
|
63
|
+
return [
|
|
64
|
+
"//",
|
|
65
|
+
"// Keywords in this schema that did not become rules:",
|
|
66
|
+
...lines,
|
|
67
|
+
"",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
export function generateValidatorModule(schema, options) {
|
|
71
|
+
const declarations = flattenSchema(schema, readChildSchemas);
|
|
72
|
+
const fields = declarations.map((declaration) => {
|
|
73
|
+
// Flattening reports isRequired for the root's own keys only. A nested
|
|
74
|
+
// required is a rule on the object at run time, and the chain has nowhere
|
|
75
|
+
// to put that. See resolve-required-path.ts for when it can be lowered.
|
|
76
|
+
const safelyRequired = declaration.isRequired || isSafelyRequired(schema, declaration.path);
|
|
77
|
+
const field = toFieldChain(declaration.path, declaration.schema, safelyRequired);
|
|
78
|
+
if (safelyRequired || !isListedByParent(schema, declaration.path)) {
|
|
79
|
+
return field;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...field,
|
|
83
|
+
skipped: [
|
|
84
|
+
...field.skipped,
|
|
85
|
+
{
|
|
86
|
+
keyword: "required",
|
|
87
|
+
reason: "the parent schema marks it required, but an ancestor object is " +
|
|
88
|
+
"not, so .required() would wrongly reject a document missing that " +
|
|
89
|
+
"ancestor; Draft-07 applies a subschema only to a value that " +
|
|
90
|
+
"exists, so this was emitted as optional",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
const byExport = new Map();
|
|
96
|
+
for (const field of fields) {
|
|
97
|
+
for (const call of field.calls)
|
|
98
|
+
byExport.set(call.pluginExport, call.pluginSubpath);
|
|
99
|
+
}
|
|
100
|
+
const pluginExports = [...byExport.entries()]
|
|
101
|
+
.map(([name, subpath]) => ({ name, subpath }))
|
|
102
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
103
|
+
const skipped = fields.flatMap((field) => field.skipped.map((entry) => ({ ...entry, path: field.path })));
|
|
104
|
+
const uses = pluginExports.map((entry) => ` .use(${entry.name})`).join("\n");
|
|
105
|
+
const chains = fields.map(renderChain).join("\n");
|
|
106
|
+
const source = [
|
|
107
|
+
"// Generated by @maroonedog/luq-codegen. Do not edit.",
|
|
108
|
+
"// Rules are declared against the generated type, so a spec change that",
|
|
109
|
+
"// renames a field makes this file stop compiling instead of drifting.",
|
|
110
|
+
renderSkippedNotice(skipped),
|
|
111
|
+
renderImports(pluginExports, options.typeImport),
|
|
112
|
+
"",
|
|
113
|
+
`export const ${options.validatorName} = Builder()`,
|
|
114
|
+
uses,
|
|
115
|
+
` .for<${options.typeExpression}>()`,
|
|
116
|
+
chains,
|
|
117
|
+
" .build();",
|
|
118
|
+
"",
|
|
119
|
+
]
|
|
120
|
+
.filter((part) => part !== "")
|
|
121
|
+
.join("\n");
|
|
122
|
+
return {
|
|
123
|
+
source,
|
|
124
|
+
pluginExports: pluginExports.map((entry) => entry.name),
|
|
125
|
+
skipped,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChainCall, SkippedKeyword } from "./chain-call.types";
|
|
2
|
+
export interface KeywordOutcome {
|
|
3
|
+
readonly call?: ChainCall;
|
|
4
|
+
readonly skipped?: SkippedKeyword;
|
|
5
|
+
}
|
|
6
|
+
export declare function keywordToChainCall(keyword: string, value: unknown): KeywordOutcome;
|
|
7
|
+
/** Exported so a test can inspect the table itself. */
|
|
8
|
+
export declare const BOUND_KEYWORDS: readonly string[];
|
|
9
|
+
export declare const BOUND_FORMATS: readonly string[];
|
|
10
|
+
export declare const STRUCTURAL_KEYWORDS: readonly string[];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.STRUCTURAL_KEYWORDS = exports.BOUND_FORMATS = exports.BOUND_KEYWORDS = void 0;
|
|
4
|
+
exports.keywordToChainCall = keywordToChainCall;
|
|
5
|
+
/** Keyword to the method it becomes, and the plugin carrying that method. */
|
|
6
|
+
const BINDINGS = {
|
|
7
|
+
minLength: { method: "min", pluginExport: "stringMinPlugin", subpathName: "stringMin" },
|
|
8
|
+
maxLength: { method: "max", pluginExport: "stringMaxPlugin", subpathName: "stringMax" },
|
|
9
|
+
pattern: { method: "pattern", pluginExport: "stringPatternPlugin", subpathName: "stringPattern" },
|
|
10
|
+
minimum: { method: "min", pluginExport: "numberMinPlugin", subpathName: "numberMin" },
|
|
11
|
+
maximum: { method: "max", pluginExport: "numberMaxPlugin", subpathName: "numberMax" },
|
|
12
|
+
multipleOf: { method: "multipleOf", pluginExport: "numberMultipleOfPlugin", subpathName: "numberMultipleOf" },
|
|
13
|
+
minItems: { method: "minLength", pluginExport: "arrayMinLengthPlugin", subpathName: "arrayMinLength" },
|
|
14
|
+
maxItems: { method: "maxLength", pluginExport: "arrayMaxLengthPlugin", subpathName: "arrayMaxLength" },
|
|
15
|
+
uniqueItems: { method: "unique", pluginExport: "arrayUniquePlugin", subpathName: "arrayUnique" },
|
|
16
|
+
minProperties: { method: "minProperties", pluginExport: "objectMinPropertiesPlugin", subpathName: "objectMinProperties" },
|
|
17
|
+
maxProperties: { method: "maxProperties", pluginExport: "objectMaxPropertiesPlugin", subpathName: "objectMaxProperties" },
|
|
18
|
+
const: { method: "literal", pluginExport: "literalPlugin", subpathName: "literal" },
|
|
19
|
+
enum: { method: "oneOf", pluginExport: "oneOfPlugin", subpathName: "oneOf" },
|
|
20
|
+
};
|
|
21
|
+
/** A format value to its method and plugin, paired with the library's format map. */
|
|
22
|
+
const FORMAT_BINDINGS = {
|
|
23
|
+
email: { method: "email", pluginExport: "stringEmailPlugin", subpathName: "stringEmail" },
|
|
24
|
+
uuid: { method: "uuid", pluginExport: "uuidPlugin", subpathName: "uuid" },
|
|
25
|
+
uri: { method: "url", pluginExport: "stringUrlPlugin", subpathName: "stringUrl" },
|
|
26
|
+
hostname: { method: "hostname", pluginExport: "stringHostnamePlugin", subpathName: "stringHostname" },
|
|
27
|
+
ipv4: { method: "ipv4", pluginExport: "stringIpv4Plugin", subpathName: "stringIpv4" },
|
|
28
|
+
ipv6: { method: "ipv6", pluginExport: "stringIpv6Plugin", subpathName: "stringIpv6" },
|
|
29
|
+
date: { method: "date", pluginExport: "stringDatePlugin", subpathName: "stringDate" },
|
|
30
|
+
"date-time": { method: "datetime", pluginExport: "stringDatetimePlugin", subpathName: "stringDatetime" },
|
|
31
|
+
time: { method: "time", pluginExport: "stringTimePlugin", subpathName: "stringTime" },
|
|
32
|
+
duration: { method: "duration", pluginExport: "stringDurationPlugin", subpathName: "stringDuration" },
|
|
33
|
+
"json-pointer": { method: "jsonPointer", pluginExport: "stringJsonPointerPlugin", subpathName: "stringJsonPointer" },
|
|
34
|
+
iri: { method: "iri", pluginExport: "stringIriPlugin", subpathName: "stringIri" },
|
|
35
|
+
};
|
|
36
|
+
/** Handled structurally, so never turned into a chain call here. */
|
|
37
|
+
const STRUCTURAL = {
|
|
38
|
+
type: "selects the slot; it is not a method",
|
|
39
|
+
properties: "expands into declarations for the child fields",
|
|
40
|
+
items: "expands into the declaration for array elements",
|
|
41
|
+
required: "held by the parent, as presence",
|
|
42
|
+
allOf: "folded away during flattening",
|
|
43
|
+
$ref: "resolved before flattening",
|
|
44
|
+
title: "annotation; not validated",
|
|
45
|
+
description: "annotation; not validated",
|
|
46
|
+
default: "annotation; not validated",
|
|
47
|
+
example: "annotation; not validated",
|
|
48
|
+
examples: "annotation; not validated",
|
|
49
|
+
deprecated: "annotation; not validated",
|
|
50
|
+
readOnly: "an OpenAPI direction marker; not lowered into validation",
|
|
51
|
+
writeOnly: "an OpenAPI direction marker; not lowered into validation",
|
|
52
|
+
nullable: "handled as presence",
|
|
53
|
+
};
|
|
54
|
+
function subpathOf(name) {
|
|
55
|
+
return `@maroonedog/luq/plugins/${name}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Values are embedded as source, so they go through JSON.stringify. Never as
|
|
59
|
+
* a regular-expression literal: a pattern is passed as a string, and making it
|
|
60
|
+
* a literal would have the escapes interpreted twice.
|
|
61
|
+
*/
|
|
62
|
+
function renderValue(value) {
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
function keywordToChainCall(keyword, value) {
|
|
66
|
+
const structural = STRUCTURAL[keyword];
|
|
67
|
+
if (structural !== undefined) {
|
|
68
|
+
return { skipped: { keyword, reason: structural } };
|
|
69
|
+
}
|
|
70
|
+
if (keyword === "format") {
|
|
71
|
+
if (typeof value !== "string") {
|
|
72
|
+
return { skipped: { keyword, reason: "the format value is not a string" } };
|
|
73
|
+
}
|
|
74
|
+
const binding = FORMAT_BINDINGS[value];
|
|
75
|
+
if (binding === undefined) {
|
|
76
|
+
return {
|
|
77
|
+
skipped: { keyword, reason: `no plugin corresponds to format "${value}"` },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
call: {
|
|
82
|
+
method: binding.method,
|
|
83
|
+
args: [],
|
|
84
|
+
pluginExport: binding.pluginExport,
|
|
85
|
+
pluginSubpath: subpathOf(binding.subpathName),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// In Draft-07 uniqueItems: false disables the constraint, so emit no rule.
|
|
90
|
+
if (keyword === "uniqueItems" && value !== true) {
|
|
91
|
+
return { skipped: { keyword, reason: "uniqueItems: false is not a constraint" } };
|
|
92
|
+
}
|
|
93
|
+
const binding = BINDINGS[keyword];
|
|
94
|
+
if (binding === undefined) {
|
|
95
|
+
return {
|
|
96
|
+
skipped: { keyword, reason: "no chain method corresponds to it" },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
call: {
|
|
101
|
+
method: binding.method,
|
|
102
|
+
args: keyword === "uniqueItems" ? [] : [renderValue(value)],
|
|
103
|
+
pluginExport: binding.pluginExport,
|
|
104
|
+
pluginSubpath: subpathOf(binding.subpathName),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Exported so a test can inspect the table itself. */
|
|
109
|
+
exports.BOUND_KEYWORDS = Object.keys(BINDINGS);
|
|
110
|
+
exports.BOUND_FORMATS = Object.keys(FORMAT_BINDINGS);
|
|
111
|
+
exports.STRUCTURAL_KEYWORDS = Object.keys(STRUCTURAL);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Keyword to the method it becomes, and the plugin carrying that method. */
|
|
2
|
+
const BINDINGS = {
|
|
3
|
+
minLength: { method: "min", pluginExport: "stringMinPlugin", subpathName: "stringMin" },
|
|
4
|
+
maxLength: { method: "max", pluginExport: "stringMaxPlugin", subpathName: "stringMax" },
|
|
5
|
+
pattern: { method: "pattern", pluginExport: "stringPatternPlugin", subpathName: "stringPattern" },
|
|
6
|
+
minimum: { method: "min", pluginExport: "numberMinPlugin", subpathName: "numberMin" },
|
|
7
|
+
maximum: { method: "max", pluginExport: "numberMaxPlugin", subpathName: "numberMax" },
|
|
8
|
+
multipleOf: { method: "multipleOf", pluginExport: "numberMultipleOfPlugin", subpathName: "numberMultipleOf" },
|
|
9
|
+
minItems: { method: "minLength", pluginExport: "arrayMinLengthPlugin", subpathName: "arrayMinLength" },
|
|
10
|
+
maxItems: { method: "maxLength", pluginExport: "arrayMaxLengthPlugin", subpathName: "arrayMaxLength" },
|
|
11
|
+
uniqueItems: { method: "unique", pluginExport: "arrayUniquePlugin", subpathName: "arrayUnique" },
|
|
12
|
+
minProperties: { method: "minProperties", pluginExport: "objectMinPropertiesPlugin", subpathName: "objectMinProperties" },
|
|
13
|
+
maxProperties: { method: "maxProperties", pluginExport: "objectMaxPropertiesPlugin", subpathName: "objectMaxProperties" },
|
|
14
|
+
const: { method: "literal", pluginExport: "literalPlugin", subpathName: "literal" },
|
|
15
|
+
enum: { method: "oneOf", pluginExport: "oneOfPlugin", subpathName: "oneOf" },
|
|
16
|
+
};
|
|
17
|
+
/** A format value to its method and plugin, paired with the library's format map. */
|
|
18
|
+
const FORMAT_BINDINGS = {
|
|
19
|
+
email: { method: "email", pluginExport: "stringEmailPlugin", subpathName: "stringEmail" },
|
|
20
|
+
uuid: { method: "uuid", pluginExport: "uuidPlugin", subpathName: "uuid" },
|
|
21
|
+
uri: { method: "url", pluginExport: "stringUrlPlugin", subpathName: "stringUrl" },
|
|
22
|
+
hostname: { method: "hostname", pluginExport: "stringHostnamePlugin", subpathName: "stringHostname" },
|
|
23
|
+
ipv4: { method: "ipv4", pluginExport: "stringIpv4Plugin", subpathName: "stringIpv4" },
|
|
24
|
+
ipv6: { method: "ipv6", pluginExport: "stringIpv6Plugin", subpathName: "stringIpv6" },
|
|
25
|
+
date: { method: "date", pluginExport: "stringDatePlugin", subpathName: "stringDate" },
|
|
26
|
+
"date-time": { method: "datetime", pluginExport: "stringDatetimePlugin", subpathName: "stringDatetime" },
|
|
27
|
+
time: { method: "time", pluginExport: "stringTimePlugin", subpathName: "stringTime" },
|
|
28
|
+
duration: { method: "duration", pluginExport: "stringDurationPlugin", subpathName: "stringDuration" },
|
|
29
|
+
"json-pointer": { method: "jsonPointer", pluginExport: "stringJsonPointerPlugin", subpathName: "stringJsonPointer" },
|
|
30
|
+
iri: { method: "iri", pluginExport: "stringIriPlugin", subpathName: "stringIri" },
|
|
31
|
+
};
|
|
32
|
+
/** Handled structurally, so never turned into a chain call here. */
|
|
33
|
+
const STRUCTURAL = {
|
|
34
|
+
type: "selects the slot; it is not a method",
|
|
35
|
+
properties: "expands into declarations for the child fields",
|
|
36
|
+
items: "expands into the declaration for array elements",
|
|
37
|
+
required: "held by the parent, as presence",
|
|
38
|
+
allOf: "folded away during flattening",
|
|
39
|
+
$ref: "resolved before flattening",
|
|
40
|
+
title: "annotation; not validated",
|
|
41
|
+
description: "annotation; not validated",
|
|
42
|
+
default: "annotation; not validated",
|
|
43
|
+
example: "annotation; not validated",
|
|
44
|
+
examples: "annotation; not validated",
|
|
45
|
+
deprecated: "annotation; not validated",
|
|
46
|
+
readOnly: "an OpenAPI direction marker; not lowered into validation",
|
|
47
|
+
writeOnly: "an OpenAPI direction marker; not lowered into validation",
|
|
48
|
+
nullable: "handled as presence",
|
|
49
|
+
};
|
|
50
|
+
function subpathOf(name) {
|
|
51
|
+
return `@maroonedog/luq/plugins/${name}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Values are embedded as source, so they go through JSON.stringify. Never as
|
|
55
|
+
* a regular-expression literal: a pattern is passed as a string, and making it
|
|
56
|
+
* a literal would have the escapes interpreted twice.
|
|
57
|
+
*/
|
|
58
|
+
function renderValue(value) {
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
export function keywordToChainCall(keyword, value) {
|
|
62
|
+
const structural = STRUCTURAL[keyword];
|
|
63
|
+
if (structural !== undefined) {
|
|
64
|
+
return { skipped: { keyword, reason: structural } };
|
|
65
|
+
}
|
|
66
|
+
if (keyword === "format") {
|
|
67
|
+
if (typeof value !== "string") {
|
|
68
|
+
return { skipped: { keyword, reason: "the format value is not a string" } };
|
|
69
|
+
}
|
|
70
|
+
const binding = FORMAT_BINDINGS[value];
|
|
71
|
+
if (binding === undefined) {
|
|
72
|
+
return {
|
|
73
|
+
skipped: { keyword, reason: `no plugin corresponds to format "${value}"` },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
call: {
|
|
78
|
+
method: binding.method,
|
|
79
|
+
args: [],
|
|
80
|
+
pluginExport: binding.pluginExport,
|
|
81
|
+
pluginSubpath: subpathOf(binding.subpathName),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// In Draft-07 uniqueItems: false disables the constraint, so emit no rule.
|
|
86
|
+
if (keyword === "uniqueItems" && value !== true) {
|
|
87
|
+
return { skipped: { keyword, reason: "uniqueItems: false is not a constraint" } };
|
|
88
|
+
}
|
|
89
|
+
const binding = BINDINGS[keyword];
|
|
90
|
+
if (binding === undefined) {
|
|
91
|
+
return {
|
|
92
|
+
skipped: { keyword, reason: "no chain method corresponds to it" },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
call: {
|
|
97
|
+
method: binding.method,
|
|
98
|
+
args: keyword === "uniqueItems" ? [] : [renderValue(value)],
|
|
99
|
+
pluginExport: binding.pluginExport,
|
|
100
|
+
pluginSubpath: subpathOf(binding.subpathName),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Exported so a test can inspect the table itself. */
|
|
105
|
+
export const BOUND_KEYWORDS = Object.keys(BINDINGS);
|
|
106
|
+
export const BOUND_FORMATS = Object.keys(FORMAT_BINDINGS);
|
|
107
|
+
export const STRUCTURAL_KEYWORDS = Object.keys(STRUCTURAL);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Draft07Schema } from "@maroonedog/luq/schema-tooling";
|
|
2
|
+
/** "items[*].sku" -> ["items[*]", "sku"]; "[*]" stays attached to its key. */
|
|
3
|
+
export declare function splitDeclaredPath(path: string): readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* Whether `.required()` may be emitted for that path.
|
|
6
|
+
*
|
|
7
|
+
* False as soon as one ancestor object along the way is not itself required.
|
|
8
|
+
* On false the caller emits `.optional()` instead and reports the omission.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSafelyRequired(root: Draft07Schema, path: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the immediate parent schema lists that key in its required set.
|
|
13
|
+
*
|
|
14
|
+
* When the path is not safely required, this separates "it was never written
|
|
15
|
+
* as required" from "it was, but an ancestor makes it unlowerable". Only the
|
|
16
|
+
* second is reported as skipped.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isListedByParent(root: Draft07Schema, path: string): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitDeclaredPath = splitDeclaredPath;
|
|
4
|
+
exports.isSafelyRequired = isSafelyRequired;
|
|
5
|
+
exports.isListedByParent = isListedByParent;
|
|
6
|
+
const schema_tooling_1 = require("@maroonedog/luq/schema-tooling");
|
|
7
|
+
/** "items[*].sku" -> ["items[*]", "sku"]; "[*]" stays attached to its key. */
|
|
8
|
+
function splitDeclaredPath(path) {
|
|
9
|
+
return path === "" ? [] : path.split(".");
|
|
10
|
+
}
|
|
11
|
+
function propertyOf(schema, key) {
|
|
12
|
+
const properties = schema.properties;
|
|
13
|
+
if (properties === null || typeof properties !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
const child = properties[key];
|
|
16
|
+
if (!(0, schema_tooling_1.isDraft07Schema)(child))
|
|
17
|
+
return undefined;
|
|
18
|
+
return (0, schema_tooling_1.isSchemaObject)(child) ? child : undefined;
|
|
19
|
+
}
|
|
20
|
+
function itemsOf(schema) {
|
|
21
|
+
const items = schema.items;
|
|
22
|
+
if (!(0, schema_tooling_1.isDraft07Schema)(items))
|
|
23
|
+
return undefined;
|
|
24
|
+
return (0, schema_tooling_1.isSchemaObject)(items) ? items : undefined;
|
|
25
|
+
}
|
|
26
|
+
function listsAsRequired(schema, key) {
|
|
27
|
+
const required = schema.required;
|
|
28
|
+
return Array.isArray(required) && required.includes(key);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether `.required()` may be emitted for that path.
|
|
32
|
+
*
|
|
33
|
+
* False as soon as one ancestor object along the way is not itself required.
|
|
34
|
+
* On false the caller emits `.optional()` instead and reports the omission.
|
|
35
|
+
*/
|
|
36
|
+
function isSafelyRequired(root, path) {
|
|
37
|
+
if (!(0, schema_tooling_1.isSchemaObject)(root))
|
|
38
|
+
return false;
|
|
39
|
+
const steps = splitDeclaredPath(path);
|
|
40
|
+
if (steps.length === 0)
|
|
41
|
+
return false;
|
|
42
|
+
let current = root;
|
|
43
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
44
|
+
const step = steps[index] ?? "";
|
|
45
|
+
const wildcards = (step.match(/\[\*\]/g) ?? []).length;
|
|
46
|
+
const key = step.slice(0, step.length - wildcards * 3);
|
|
47
|
+
if (!listsAsRequired(current, key))
|
|
48
|
+
return false;
|
|
49
|
+
const child = propertyOf(current, key);
|
|
50
|
+
if (child === undefined)
|
|
51
|
+
return index === steps.length - 1;
|
|
52
|
+
let descended = child;
|
|
53
|
+
for (let level = 0; level < wildcards; level += 1) {
|
|
54
|
+
const element = itemsOf(descended);
|
|
55
|
+
// With no element schema there is nowhere further to walk. Every
|
|
56
|
+
// ancestor so far was required, so the last segment may be required.
|
|
57
|
+
if (element === undefined)
|
|
58
|
+
return index === steps.length - 1;
|
|
59
|
+
descended = element;
|
|
60
|
+
}
|
|
61
|
+
current = descended;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the immediate parent schema lists that key in its required set.
|
|
67
|
+
*
|
|
68
|
+
* When the path is not safely required, this separates "it was never written
|
|
69
|
+
* as required" from "it was, but an ancestor makes it unlowerable". Only the
|
|
70
|
+
* second is reported as skipped.
|
|
71
|
+
*/
|
|
72
|
+
function isListedByParent(root, path) {
|
|
73
|
+
if (!(0, schema_tooling_1.isSchemaObject)(root))
|
|
74
|
+
return false;
|
|
75
|
+
const steps = splitDeclaredPath(path);
|
|
76
|
+
if (steps.length === 0)
|
|
77
|
+
return false;
|
|
78
|
+
let current = root;
|
|
79
|
+
for (let index = 0; index < steps.length - 1; index += 1) {
|
|
80
|
+
const step = steps[index] ?? "";
|
|
81
|
+
const wildcards = (step.match(/\[\*\]/g) ?? []).length;
|
|
82
|
+
const key = step.slice(0, step.length - wildcards * 3);
|
|
83
|
+
const child = propertyOf(current, key);
|
|
84
|
+
if (child === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
let descended = child;
|
|
87
|
+
for (let level = 0; level < wildcards; level += 1) {
|
|
88
|
+
const element = itemsOf(descended);
|
|
89
|
+
if (element === undefined)
|
|
90
|
+
return false;
|
|
91
|
+
descended = element;
|
|
92
|
+
}
|
|
93
|
+
current = descended;
|
|
94
|
+
}
|
|
95
|
+
const last = steps[steps.length - 1] ?? "";
|
|
96
|
+
const wildcards = (last.match(/\[\*\]/g) ?? []).length;
|
|
97
|
+
return listsAsRequired(current, last.slice(0, last.length - wildcards * 3));
|
|
98
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isDraft07Schema, isSchemaObject, } from "@maroonedog/luq/schema-tooling";
|
|
2
|
+
/** "items[*].sku" -> ["items[*]", "sku"]; "[*]" stays attached to its key. */
|
|
3
|
+
export function splitDeclaredPath(path) {
|
|
4
|
+
return path === "" ? [] : path.split(".");
|
|
5
|
+
}
|
|
6
|
+
function propertyOf(schema, key) {
|
|
7
|
+
const properties = schema.properties;
|
|
8
|
+
if (properties === null || typeof properties !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const child = properties[key];
|
|
11
|
+
if (!isDraft07Schema(child))
|
|
12
|
+
return undefined;
|
|
13
|
+
return isSchemaObject(child) ? child : undefined;
|
|
14
|
+
}
|
|
15
|
+
function itemsOf(schema) {
|
|
16
|
+
const items = schema.items;
|
|
17
|
+
if (!isDraft07Schema(items))
|
|
18
|
+
return undefined;
|
|
19
|
+
return isSchemaObject(items) ? items : undefined;
|
|
20
|
+
}
|
|
21
|
+
function listsAsRequired(schema, key) {
|
|
22
|
+
const required = schema.required;
|
|
23
|
+
return Array.isArray(required) && required.includes(key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether `.required()` may be emitted for that path.
|
|
27
|
+
*
|
|
28
|
+
* False as soon as one ancestor object along the way is not itself required.
|
|
29
|
+
* On false the caller emits `.optional()` instead and reports the omission.
|
|
30
|
+
*/
|
|
31
|
+
export function isSafelyRequired(root, path) {
|
|
32
|
+
if (!isSchemaObject(root))
|
|
33
|
+
return false;
|
|
34
|
+
const steps = splitDeclaredPath(path);
|
|
35
|
+
if (steps.length === 0)
|
|
36
|
+
return false;
|
|
37
|
+
let current = root;
|
|
38
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
39
|
+
const step = steps[index] ?? "";
|
|
40
|
+
const wildcards = (step.match(/\[\*\]/g) ?? []).length;
|
|
41
|
+
const key = step.slice(0, step.length - wildcards * 3);
|
|
42
|
+
if (!listsAsRequired(current, key))
|
|
43
|
+
return false;
|
|
44
|
+
const child = propertyOf(current, key);
|
|
45
|
+
if (child === undefined)
|
|
46
|
+
return index === steps.length - 1;
|
|
47
|
+
let descended = child;
|
|
48
|
+
for (let level = 0; level < wildcards; level += 1) {
|
|
49
|
+
const element = itemsOf(descended);
|
|
50
|
+
// With no element schema there is nowhere further to walk. Every
|
|
51
|
+
// ancestor so far was required, so the last segment may be required.
|
|
52
|
+
if (element === undefined)
|
|
53
|
+
return index === steps.length - 1;
|
|
54
|
+
descended = element;
|
|
55
|
+
}
|
|
56
|
+
current = descended;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether the immediate parent schema lists that key in its required set.
|
|
62
|
+
*
|
|
63
|
+
* When the path is not safely required, this separates "it was never written
|
|
64
|
+
* as required" from "it was, but an ancestor makes it unlowerable". Only the
|
|
65
|
+
* second is reported as skipped.
|
|
66
|
+
*/
|
|
67
|
+
export function isListedByParent(root, path) {
|
|
68
|
+
if (!isSchemaObject(root))
|
|
69
|
+
return false;
|
|
70
|
+
const steps = splitDeclaredPath(path);
|
|
71
|
+
if (steps.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
let current = root;
|
|
74
|
+
for (let index = 0; index < steps.length - 1; index += 1) {
|
|
75
|
+
const step = steps[index] ?? "";
|
|
76
|
+
const wildcards = (step.match(/\[\*\]/g) ?? []).length;
|
|
77
|
+
const key = step.slice(0, step.length - wildcards * 3);
|
|
78
|
+
const child = propertyOf(current, key);
|
|
79
|
+
if (child === undefined)
|
|
80
|
+
return false;
|
|
81
|
+
let descended = child;
|
|
82
|
+
for (let level = 0; level < wildcards; level += 1) {
|
|
83
|
+
const element = itemsOf(descended);
|
|
84
|
+
if (element === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
descended = element;
|
|
87
|
+
}
|
|
88
|
+
current = descended;
|
|
89
|
+
}
|
|
90
|
+
const last = steps[steps.length - 1] ?? "";
|
|
91
|
+
const wildcards = (last.match(/\[\*\]/g) ?? []).length;
|
|
92
|
+
return listsAsRequired(current, last.slice(0, last.length - wildcards * 3));
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Draft07SchemaObject } from "@maroonedog/luq/schema-tooling";
|
|
2
|
+
/**
|
|
3
|
+
* A schema with no type, with several types, or with null alone belongs to no
|
|
4
|
+
* slot, so it falls to "any".
|
|
5
|
+
*
|
|
6
|
+
* That narrows what can be written on it. A plugin serves the slots it
|
|
7
|
+
* declares, and the ones declaring "any" are the type-agnostic rules —
|
|
8
|
+
* presence and the value comparisons. A rule that belongs to one type does
|
|
9
|
+
* not appear: `oneOf` serves string, number and boolean, so a schema whose
|
|
10
|
+
* type could not be decided gets no `oneOf` call and the keyword is reported
|
|
11
|
+
* skipped rather than emitted onto a slot that would not compile.
|
|
12
|
+
*/
|
|
13
|
+
export declare function slotForSchema(schema: Draft07SchemaObject): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.slotForSchema = slotForSchema;
|
|
4
|
+
const SLOT_BY_TYPE = {
|
|
5
|
+
string: "string",
|
|
6
|
+
number: "number",
|
|
7
|
+
integer: "number",
|
|
8
|
+
boolean: "boolean",
|
|
9
|
+
array: "array",
|
|
10
|
+
object: "object",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A schema with no type, with several types, or with null alone belongs to no
|
|
14
|
+
* slot, so it falls to "any".
|
|
15
|
+
*
|
|
16
|
+
* That narrows what can be written on it. A plugin serves the slots it
|
|
17
|
+
* declares, and the ones declaring "any" are the type-agnostic rules —
|
|
18
|
+
* presence and the value comparisons. A rule that belongs to one type does
|
|
19
|
+
* not appear: `oneOf` serves string, number and boolean, so a schema whose
|
|
20
|
+
* type could not be decided gets no `oneOf` call and the keyword is reported
|
|
21
|
+
* skipped rather than emitted onto a slot that would not compile.
|
|
22
|
+
*/
|
|
23
|
+
function slotForSchema(schema) {
|
|
24
|
+
const type = schema.type;
|
|
25
|
+
if (typeof type !== "string")
|
|
26
|
+
return "any";
|
|
27
|
+
return SLOT_BY_TYPE[type] ?? "any";
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SLOT_BY_TYPE = {
|
|
2
|
+
string: "string",
|
|
3
|
+
number: "number",
|
|
4
|
+
integer: "number",
|
|
5
|
+
boolean: "boolean",
|
|
6
|
+
array: "array",
|
|
7
|
+
object: "object",
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A schema with no type, with several types, or with null alone belongs to no
|
|
11
|
+
* slot, so it falls to "any".
|
|
12
|
+
*
|
|
13
|
+
* That narrows what can be written on it. A plugin serves the slots it
|
|
14
|
+
* declares, and the ones declaring "any" are the type-agnostic rules —
|
|
15
|
+
* presence and the value comparisons. A rule that belongs to one type does
|
|
16
|
+
* not appear: `oneOf` serves string, number and boolean, so a schema whose
|
|
17
|
+
* type could not be decided gets no `oneOf` call and the keyword is reported
|
|
18
|
+
* skipped rather than emitted onto a slot that would not compile.
|
|
19
|
+
*/
|
|
20
|
+
export function slotForSchema(schema) {
|
|
21
|
+
const type = schema.type;
|
|
22
|
+
if (typeof type !== "string")
|
|
23
|
+
return "any";
|
|
24
|
+
return SLOT_BY_TYPE[type] ?? "any";
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateValidatorModule = void 0;
|
|
4
|
+
// ===========================================================================
|
|
5
|
+
// luq-codegen/src/index.ts — re-exports only. Nothing is defined here.
|
|
6
|
+
//
|
|
7
|
+
// The one entry point this package publishes. Every module beside it is
|
|
8
|
+
// reachable only through what is named below, so moving or splitting one of
|
|
9
|
+
// them is not a breaking change for anybody.
|
|
10
|
+
// ===========================================================================
|
|
11
|
+
var generate_validator_module_1 = require("./generate/generate-validator-module");
|
|
12
|
+
Object.defineProperty(exports, "generateValidatorModule", { enumerable: true, get: function () { return generate_validator_module_1.generateValidatorModule; } });
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// luq-codegen/src/index.ts — re-exports only. Nothing is defined here.
|
|
3
|
+
//
|
|
4
|
+
// The one entry point this package publishes. Every module beside it is
|
|
5
|
+
// reachable only through what is named below, so moving or splitting one of
|
|
6
|
+
// them is not a breaking change for anybody.
|
|
7
|
+
// ===========================================================================
|
|
8
|
+
export { generateValidatorModule } from "./generate/generate-validator-module.mjs";
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maroonedog/luq-codegen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generates the source of a Luq validator module from a JSON Schema Draft-07 object, such as an OpenAPI 3.0 component schema.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/maroonedog/luq.git",
|
|
9
|
+
"directory": "luq-codegen"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"module": "dist/index.mjs",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist"],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "npx ts-node --project ../scripts/tsconfig.json ../scripts/build-luq-codegen.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
27
|
+
"test": "jest --no-watch --no-watchman -c jest.config.cjs",
|
|
28
|
+
"check:no-dynamic-code": "npx ts-node --project ../scripts/tsconfig.json ../scripts/check-no-dynamic-code.ts luq-codegen",
|
|
29
|
+
"check:dist-layout": "npx ts-node --project ../scripts/tsconfig.json ../scripts/check-dist-layout.ts luq-codegen",
|
|
30
|
+
"check:dist": "npm run check:no-dynamic-code && npm run check:dist-layout",
|
|
31
|
+
"verify": "npm run typecheck && npm run build && npm run check:dist && npm test",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"prepublishOnly": "npm run verify"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@maroonedog/luq": ">=2.4.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@maroonedog/luq": "file:.."
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|