@zipbul/baker 2.2.0 → 3.0.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/CHANGELOG.md +256 -0
- package/MIGRATION-3.0.md +104 -0
- package/README.md +109 -63
- package/dist/index.d.ts +7 -6
- package/dist/index.js +10 -321
- package/dist/src/collect.d.ts +13 -10
- package/dist/src/collect.js +26 -0
- package/dist/src/configure.d.ts +8 -6
- package/dist/src/configure.js +43 -0
- package/dist/src/create-rule.js +41 -0
- package/dist/src/decorators/field.d.ts +22 -18
- package/dist/src/decorators/field.js +268 -0
- package/dist/src/decorators/index.d.ts +1 -0
- package/dist/src/decorators/index.js +2 -2
- package/dist/src/decorators/recipe.d.ts +17 -0
- package/dist/src/decorators/recipe.js +23 -0
- package/dist/src/errors.d.ts +27 -17
- package/dist/src/errors.js +52 -0
- package/dist/src/functions/check-call-options.d.ts +8 -0
- package/dist/src/functions/check-call-options.js +51 -0
- package/dist/src/functions/deserialize.d.ts +13 -6
- package/dist/src/functions/deserialize.js +57 -0
- package/dist/src/functions/serialize.d.ts +10 -4
- package/dist/src/functions/serialize.js +52 -0
- package/dist/src/functions/validate.d.ts +13 -10
- package/dist/src/functions/validate.js +49 -0
- package/dist/src/interfaces.d.ts +1 -1
- package/dist/src/interfaces.js +4 -0
- package/dist/src/meta-access.d.ts +19 -0
- package/dist/src/meta-access.js +75 -0
- package/dist/src/registry.js +8 -0
- package/dist/src/rule-metadata.d.ts +11 -0
- package/dist/src/rule-metadata.js +17 -0
- package/dist/src/rule-plan.d.ts +10 -11
- package/dist/src/rule-plan.js +117 -0
- package/dist/src/rules/array.d.ts +7 -6
- package/dist/src/rules/array.js +96 -0
- package/dist/src/rules/common.js +77 -0
- package/dist/src/rules/date.js +35 -0
- package/dist/src/rules/index.d.ts +2 -4
- package/dist/src/rules/index.js +8 -21
- package/dist/src/rules/locales.d.ts +5 -4
- package/dist/src/rules/locales.js +249 -0
- package/dist/src/rules/number.js +79 -0
- package/dist/src/rules/object.d.ts +1 -1
- package/dist/src/rules/object.js +49 -0
- package/dist/src/rules/string.d.ts +83 -80
- package/dist/src/rules/string.js +1998 -0
- package/dist/src/rules/typechecker.js +143 -0
- package/dist/src/seal/circular-analyzer.js +63 -0
- package/dist/src/seal/codegen-utils.js +18 -0
- package/dist/src/seal/deserialize-builder.d.ts +8 -4
- package/dist/src/seal/deserialize-builder.js +1546 -0
- package/dist/src/seal/expose-validator.d.ts +3 -2
- package/dist/src/seal/expose-validator.js +65 -0
- package/dist/src/seal/seal-state.d.ts +10 -0
- package/dist/src/seal/seal-state.js +18 -0
- package/dist/src/seal/seal.d.ts +22 -21
- package/dist/src/seal/seal.js +431 -0
- package/dist/src/seal/serialize-builder.d.ts +3 -2
- package/dist/src/seal/serialize-builder.js +374 -0
- package/dist/src/seal/validate-meta.d.ts +13 -0
- package/dist/src/seal/validate-meta.js +61 -0
- package/dist/src/symbols.d.ts +1 -1
- package/dist/src/symbols.js +13 -2
- package/dist/src/transformers/collection.transformer.js +25 -0
- package/dist/src/transformers/date.transformer.js +18 -0
- package/dist/src/transformers/index.js +6 -2
- package/dist/src/transformers/luxon.transformer.d.ts +4 -2
- package/dist/src/transformers/luxon.transformer.js +34 -0
- package/dist/src/transformers/moment.transformer.d.ts +4 -2
- package/dist/src/transformers/moment.transformer.js +32 -0
- package/dist/src/transformers/number.transformer.js +8 -0
- package/dist/src/transformers/string.transformer.js +12 -0
- package/dist/src/types.d.ts +27 -25
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +2 -2
- package/dist/src/utils.js +10 -0
- package/package.json +80 -67
- package/dist/index-03cysbck.js +0 -3
- package/dist/index-dcbd798a.js +0 -3
- package/dist/index-jp2yjd6g.js +0 -3
- package/dist/index-mw7met6r.js +0 -3
- package/dist/index-xdn55cz3.js +0 -1
- package/dist/src/functions/_run-sealed.d.ts +0 -7
- package/dist/src/functions/index.d.ts +0 -3
- package/dist/src/seal/index.d.ts +0 -5
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { RuntimeOptions } from '../interfaces';
|
|
2
2
|
/**
|
|
3
3
|
* Converts a Class instance to a plain object.
|
|
4
|
-
* -
|
|
4
|
+
* - Requires `seal()` to be called beforehand; throws `BakerError` if not sealed
|
|
5
5
|
* - Sync DTOs return directly; async DTOs return Promise
|
|
6
6
|
* - No validation — always returns Record<string, unknown>
|
|
7
|
-
* - Class without decorators: throws SealError
|
|
8
7
|
*/
|
|
9
|
-
export declare function serialize<T>(instance: T, options?: RuntimeOptions): Record<string, unknown
|
|
10
|
-
|
|
8
|
+
export declare function serialize<T>(instance: T, options?: RuntimeOptions): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
9
|
+
/**
|
|
10
|
+
* Sync-asserted serialize. Throws `BakerError` if Class has any async transform on the serialize side.
|
|
11
|
+
*/
|
|
12
|
+
export declare function serializeSync<T>(instance: T, options?: RuntimeOptions): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Async-asserted serialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
15
|
+
*/
|
|
16
|
+
export declare function serializeAsync<T>(instance: T, options?: RuntimeOptions): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BakerError } from '../errors.js';
|
|
2
|
+
import { ensureSealed } from '../seal/seal.js';
|
|
3
|
+
import { checkCallOptions } from './check-call-options.js';
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// serialize — Public API (§5.2)
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
/** Boundary check shared by serialize / serializeSync / serializeAsync. */
|
|
8
|
+
function resolveSerializer(instance, fnName) {
|
|
9
|
+
if (instance == null || typeof instance !== 'object') {
|
|
10
|
+
throw new BakerError(`${fnName}: expected a class instance, got ${instance === null ? 'null' : typeof instance}`);
|
|
11
|
+
}
|
|
12
|
+
const Class = instance.constructor;
|
|
13
|
+
if (typeof Class !== 'function') {
|
|
14
|
+
throw new BakerError(`${fnName}: instance has no constructor`);
|
|
15
|
+
}
|
|
16
|
+
// Reject plain objects and forged ones (e.g. `{ constructor: SomeDto }`): a real instance is
|
|
17
|
+
// `instanceof` its own constructor via the prototype chain; the `constructor` property alone
|
|
18
|
+
// (which anyone can set) is not trusted.
|
|
19
|
+
if (Class === Object || !(instance instanceof Class)) {
|
|
20
|
+
throw new BakerError(`${fnName}: received a plain object. Pass an instance of a DTO class decorated with @Field.`);
|
|
21
|
+
}
|
|
22
|
+
return ensureSealed(Class);
|
|
23
|
+
}
|
|
24
|
+
export function serialize(instance, options) {
|
|
25
|
+
const checkedOpts = checkCallOptions(options);
|
|
26
|
+
const sealed = resolveSerializer(instance, 'serialize');
|
|
27
|
+
return sealed.isSerializeAsync
|
|
28
|
+
? sealed.serialize(instance, checkedOpts)
|
|
29
|
+
: sealed.serialize(instance, checkedOpts);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sync-asserted serialize. Throws `BakerError` if Class has any async transform on the serialize side.
|
|
33
|
+
*/
|
|
34
|
+
export function serializeSync(instance, options) {
|
|
35
|
+
const checkedOpts = checkCallOptions(options);
|
|
36
|
+
const sealed = resolveSerializer(instance, 'serializeSync');
|
|
37
|
+
if (sealed.isSerializeAsync) {
|
|
38
|
+
const className = instance.constructor.name;
|
|
39
|
+
throw new BakerError(`serializeSync(${className}): DTO has async serialize transforms. Use serializeAsync() instead.`);
|
|
40
|
+
}
|
|
41
|
+
return sealed.serialize(instance, checkedOpts);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Async-asserted serialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
45
|
+
*/
|
|
46
|
+
export function serializeAsync(instance, options) {
|
|
47
|
+
const checkedOpts = checkCallOptions(options);
|
|
48
|
+
const sealed = resolveSerializer(instance, 'serializeAsync');
|
|
49
|
+
return sealed.isSerializeAsync
|
|
50
|
+
? sealed.serialize(instance, checkedOpts)
|
|
51
|
+
: Promise.resolve(sealed.serialize(instance, checkedOpts));
|
|
52
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { EmittableRule } from '../types';
|
|
1
|
+
import type { BakerIssueSet } from '../errors';
|
|
3
2
|
import type { RuntimeOptions } from '../interfaces';
|
|
4
3
|
/**
|
|
5
|
-
* DTO-level validation — validates input against a decorated class.
|
|
6
|
-
* Sync DTOs return directly; async DTOs return Promise.
|
|
4
|
+
* DTO-level validation — validates `input` against a decorated class's schema.
|
|
5
|
+
* Sync DTOs return directly; async DTOs return Promise. To validate a single primitive without a
|
|
6
|
+
* DTO, call the rule directly (e.g. `isEmail()(value)`).
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
export declare function validate<T>(Class: new (...args: any[]) => T, input: unknown, options?: RuntimeOptions): Promise<true | BakerErrors>;
|
|
8
|
+
declare function validate<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): true | BakerIssueSet | Promise<true | BakerIssueSet>;
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Sync-asserted validate. Throws `BakerError` if Class has any async rule/transform
|
|
11
|
+
* on the deserialize/validate side. Use when caller code assumes sync return.
|
|
13
12
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
declare function validateSync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): true | BakerIssueSet;
|
|
14
|
+
/**
|
|
15
|
+
* Async-asserted validate. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
16
|
+
*/
|
|
17
|
+
declare function validateAsync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): Promise<true | BakerIssueSet>;
|
|
18
|
+
export { validate, validateSync, validateAsync };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { toBakerIssueSet, BakerError } from '../errors.js';
|
|
2
|
+
import { ensureSealed } from '../seal/seal.js';
|
|
3
|
+
import { checkCallOptions } from './check-call-options.js';
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// validate — Public API (§5.3)
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* DTO-level validation — validates `input` against a decorated class's schema.
|
|
9
|
+
* Sync DTOs return directly; async DTOs return Promise. To validate a single primitive without a
|
|
10
|
+
* DTO, call the rule directly (e.g. `isEmail()(value)`).
|
|
11
|
+
*/
|
|
12
|
+
function validate(Class, input, options) {
|
|
13
|
+
const checkedOpts = checkCallOptions(options);
|
|
14
|
+
const sealed = ensureSealed(Class);
|
|
15
|
+
if (sealed.isAsync) {
|
|
16
|
+
return sealed.validate(input, checkedOpts).then((result) => result === null ? true : toBakerIssueSet(result));
|
|
17
|
+
}
|
|
18
|
+
const result = sealed.validate(input, checkedOpts);
|
|
19
|
+
return result === null ? true : toBakerIssueSet(result);
|
|
20
|
+
}
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// W14: strict sync/async variants — explicit intent at call site
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Sync-asserted validate. Throws `BakerError` if Class has any async rule/transform
|
|
26
|
+
* on the deserialize/validate side. Use when caller code assumes sync return.
|
|
27
|
+
*/
|
|
28
|
+
function validateSync(Class, input, options) {
|
|
29
|
+
const checkedOpts = checkCallOptions(options);
|
|
30
|
+
const sealed = ensureSealed(Class);
|
|
31
|
+
if (sealed.isAsync) {
|
|
32
|
+
throw new BakerError(`validateSync(${Class.name}): DTO has async rules/transforms. Use validateAsync() instead.`);
|
|
33
|
+
}
|
|
34
|
+
const result = sealed.validate(input, checkedOpts);
|
|
35
|
+
return result === null ? true : toBakerIssueSet(result);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Async-asserted validate. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
39
|
+
*/
|
|
40
|
+
function validateAsync(Class, input, options) {
|
|
41
|
+
const checkedOpts = checkCallOptions(options);
|
|
42
|
+
const sealed = ensureSealed(Class);
|
|
43
|
+
if (sealed.isAsync) {
|
|
44
|
+
return sealed.validate(input, checkedOpts).then((r) => r === null ? true : toBakerIssueSet(r));
|
|
45
|
+
}
|
|
46
|
+
const result = sealed.validate(input, checkedOpts);
|
|
47
|
+
return Promise.resolve(result === null ? true : toBakerIssueSet(result));
|
|
48
|
+
}
|
|
49
|
+
export { validate, validateSync, validateAsync };
|
package/dist/src/interfaces.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface SealOptions {
|
|
|
16
16
|
stopAtFirstError?: boolean;
|
|
17
17
|
/**
|
|
18
18
|
* true: reject undeclared fields. Uses the key set from mergeInheritance(Class) as the allowlist.
|
|
19
|
-
*
|
|
19
|
+
* `@Exclude` fields are also included in the whitelist — their presence is allowed but they are excluded from the result.
|
|
20
20
|
* @default false
|
|
21
21
|
*/
|
|
22
22
|
whitelist?: boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RawClassMeta, SealedExecutors } from './types';
|
|
2
|
+
export declare function getSealed(cls: Function): SealedExecutors<unknown> | undefined;
|
|
3
|
+
/** Same as getSealed but throws if the class is not sealed — for callers that establish the invariant elsewhere. */
|
|
4
|
+
export declare function requireSealed(cls: Function): SealedExecutors<unknown>;
|
|
5
|
+
export declare function setSealed(cls: Function, exec: SealedExecutors<unknown>): void;
|
|
6
|
+
export declare function hasSealedOwn(cls: Function): boolean;
|
|
7
|
+
export declare function deleteSealed(cls: Function): void;
|
|
8
|
+
export declare function deleteRaw(cls: Function): void;
|
|
9
|
+
export declare function getRaw(cls: Function): RawClassMeta | undefined;
|
|
10
|
+
/** Same as getRaw but throws if the class has no @Field decorators — for callers that establish the invariant elsewhere. */
|
|
11
|
+
export declare function requireRaw(cls: Function): RawClassMeta;
|
|
12
|
+
export declare function setRaw(cls: Function, raw: RawClassMeta): void;
|
|
13
|
+
/**
|
|
14
|
+
* True only when cls has its OWN RAW slot. A subclass without its own @Field decorators
|
|
15
|
+
* resolves Class[Symbol.metadata] to the parent's metadata via the class prototype chain;
|
|
16
|
+
* the dual own-check keeps mergeInheritance from double-counting inherited fields.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasRawOwn(cls: Function): boolean;
|
|
19
|
+
export declare function freezeRaw(cls: Function): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { RAW, SEALED } from './symbols.js';
|
|
2
|
+
/** Returns the metadata object visible on cls (own or inherited via the class prototype chain). */
|
|
3
|
+
function metaOf(cls) {
|
|
4
|
+
return cls[Symbol.metadata] ?? undefined;
|
|
5
|
+
}
|
|
6
|
+
/** Returns the class's own metadata object, creating one if absent. */
|
|
7
|
+
function ensureOwnMeta(cls) {
|
|
8
|
+
if (!Object.hasOwn(cls, Symbol.metadata)) {
|
|
9
|
+
Object.defineProperty(cls, Symbol.metadata, {
|
|
10
|
+
value: {},
|
|
11
|
+
writable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
enumerable: false,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return cls[Symbol.metadata];
|
|
17
|
+
}
|
|
18
|
+
export function getSealed(cls) {
|
|
19
|
+
return cls[SEALED];
|
|
20
|
+
}
|
|
21
|
+
/** Same as getSealed but throws if the class is not sealed — for callers that establish the invariant elsewhere. */
|
|
22
|
+
export function requireSealed(cls) {
|
|
23
|
+
const v = cls[SEALED];
|
|
24
|
+
if (v === undefined) {
|
|
25
|
+
throw new Error(`${cls.name || '<anonymous>'}: class is not sealed`);
|
|
26
|
+
}
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
export function setSealed(cls, exec) {
|
|
30
|
+
cls[SEALED] = exec;
|
|
31
|
+
}
|
|
32
|
+
export function hasSealedOwn(cls) {
|
|
33
|
+
return Object.hasOwn(cls, SEALED);
|
|
34
|
+
}
|
|
35
|
+
export function deleteSealed(cls) {
|
|
36
|
+
delete cls[SEALED];
|
|
37
|
+
}
|
|
38
|
+
export function deleteRaw(cls) {
|
|
39
|
+
if (Object.hasOwn(cls, Symbol.metadata)) {
|
|
40
|
+
delete cls[Symbol.metadata][RAW];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function getRaw(cls) {
|
|
44
|
+
return metaOf(cls)?.[RAW];
|
|
45
|
+
}
|
|
46
|
+
/** Same as getRaw but throws if the class has no @Field decorators — for callers that establish the invariant elsewhere. */
|
|
47
|
+
export function requireRaw(cls) {
|
|
48
|
+
const v = getRaw(cls);
|
|
49
|
+
if (v === undefined) {
|
|
50
|
+
throw new Error(`${cls.name || '<anonymous>'}: class has no @Field decorators`);
|
|
51
|
+
}
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
export function setRaw(cls, raw) {
|
|
55
|
+
ensureOwnMeta(cls)[RAW] = raw;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True only when cls has its OWN RAW slot. A subclass without its own @Field decorators
|
|
59
|
+
* resolves Class[Symbol.metadata] to the parent's metadata via the class prototype chain;
|
|
60
|
+
* the dual own-check keeps mergeInheritance from double-counting inherited fields.
|
|
61
|
+
*/
|
|
62
|
+
export function hasRawOwn(cls) {
|
|
63
|
+
if (!Object.hasOwn(cls, Symbol.metadata)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const meta = cls[Symbol.metadata];
|
|
67
|
+
return meta != null && Object.hasOwn(meta, RAW);
|
|
68
|
+
}
|
|
69
|
+
export function freezeRaw(cls) {
|
|
70
|
+
// Guard on own RAW: an inherited-only subclass must not freeze the parent's RAW.
|
|
71
|
+
if (!hasRawOwn(cls)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
Object.freeze(getRaw(cls));
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global registry — automatically registers classes with at least one decorator attached
|
|
3
|
+
*
|
|
4
|
+
* - Automatically called from ensureMeta()
|
|
5
|
+
* - seal() iterates this Set to seal all DTOs
|
|
6
|
+
* - Metadata is not stored here — used only as an index (which classes are registered)
|
|
7
|
+
*/
|
|
8
|
+
export const globalRegistry = new Set();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EmittableRule, InternalRule, RulePlan } from './types';
|
|
2
|
+
interface RuleMetadata {
|
|
3
|
+
emit: EmittableRule['emit'];
|
|
4
|
+
ruleName: string;
|
|
5
|
+
requiresType?: EmittableRule['requiresType'];
|
|
6
|
+
constraints?: Record<string, unknown>;
|
|
7
|
+
isAsync?: boolean;
|
|
8
|
+
plan?: RulePlan;
|
|
9
|
+
}
|
|
10
|
+
export declare function defineRuleMetadata(fn: InternalRule, meta: RuleMetadata): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function defineRuleMetadata(fn, meta) {
|
|
2
|
+
const target = fn;
|
|
3
|
+
target.emit = meta.emit;
|
|
4
|
+
target.ruleName = meta.ruleName;
|
|
5
|
+
if (meta.requiresType !== undefined) {
|
|
6
|
+
target.requiresType = meta.requiresType;
|
|
7
|
+
}
|
|
8
|
+
if (meta.constraints !== undefined) {
|
|
9
|
+
target.constraints = meta.constraints;
|
|
10
|
+
}
|
|
11
|
+
if (meta.isAsync !== undefined) {
|
|
12
|
+
target.isAsync = meta.isAsync;
|
|
13
|
+
}
|
|
14
|
+
if (meta.plan) {
|
|
15
|
+
target.plan = meta.plan;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/src/rule-plan.d.ts
CHANGED
|
@@ -3,21 +3,20 @@ type RulePlanCache = {
|
|
|
3
3
|
length?: string;
|
|
4
4
|
time?: string;
|
|
5
5
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export declare function makePlannedRule(options: {
|
|
6
|
+
declare const planValue: () => RulePlanExpr;
|
|
7
|
+
declare const planLength: (object?: RulePlanExpr) => RulePlanExpr;
|
|
8
|
+
declare const planTime: (object?: RulePlanExpr) => RulePlanExpr;
|
|
9
|
+
declare const planLiteral: (value: number) => RulePlanExpr;
|
|
10
|
+
declare const planCompare: (left: RulePlanExpr, op: "<" | "<=" | ">" | ">=" | "===" | "!==", right: number | RulePlanExpr) => RulePlanCheck;
|
|
11
|
+
declare const planOr: (...checks: RulePlanCheck[]) => RulePlanCheck;
|
|
12
|
+
declare function makePlannedRule(options: {
|
|
14
13
|
name: string;
|
|
15
14
|
requiresType: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object';
|
|
16
15
|
constraints?: Record<string, unknown>;
|
|
17
16
|
plan: RulePlan;
|
|
18
17
|
validate: (value: unknown) => boolean;
|
|
19
18
|
}): InternalRule;
|
|
20
|
-
|
|
19
|
+
declare function makeRule(options: {
|
|
21
20
|
name: string;
|
|
22
21
|
validate: (value: unknown) => boolean | Promise<boolean>;
|
|
23
22
|
emit: (varName: string, ctx: EmitContext) => string;
|
|
@@ -26,5 +25,5 @@ export declare function makeRule(options: {
|
|
|
26
25
|
isAsync?: boolean;
|
|
27
26
|
plan?: RulePlan;
|
|
28
27
|
}): InternalRule;
|
|
29
|
-
|
|
30
|
-
export {};
|
|
28
|
+
declare function emitRulePlan(varName: string, ctx: EmitContext, ruleName: string, plan: RulePlan, cache?: RulePlanCache, insideTypeGate?: boolean): string;
|
|
29
|
+
export { planValue, planLength, planTime, planLiteral, planCompare, planOr, makePlannedRule, makeRule, emitRulePlan };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { defineRuleMetadata } from './rule-metadata.js';
|
|
2
|
+
const planValue = () => ({ kind: 'value' });
|
|
3
|
+
const planLength = (object = planValue()) => ({
|
|
4
|
+
kind: 'member',
|
|
5
|
+
object,
|
|
6
|
+
property: 'length',
|
|
7
|
+
});
|
|
8
|
+
const planTime = (object = planValue()) => ({
|
|
9
|
+
kind: 'call0',
|
|
10
|
+
object,
|
|
11
|
+
method: 'getTime',
|
|
12
|
+
});
|
|
13
|
+
const planLiteral = (value) => ({ kind: 'literal', value });
|
|
14
|
+
const planCompare = (left, op, right) => ({
|
|
15
|
+
kind: 'compare',
|
|
16
|
+
left,
|
|
17
|
+
op,
|
|
18
|
+
right: typeof right === 'number' ? planLiteral(right) : right,
|
|
19
|
+
});
|
|
20
|
+
const planOr = (...checks) => ({ kind: 'or', checks });
|
|
21
|
+
function makePlannedRule(options) {
|
|
22
|
+
const inner = {
|
|
23
|
+
name: options.name,
|
|
24
|
+
requiresType: options.requiresType,
|
|
25
|
+
plan: options.plan,
|
|
26
|
+
validate: options.validate,
|
|
27
|
+
emit: (varName, ctx) => emitRulePlan(varName, ctx, options.name, options.plan, undefined, ctx.insideTypeGate),
|
|
28
|
+
};
|
|
29
|
+
if (options.constraints !== undefined) {
|
|
30
|
+
inner.constraints = options.constraints;
|
|
31
|
+
}
|
|
32
|
+
return makeRule(inner);
|
|
33
|
+
}
|
|
34
|
+
function makeRule(options) {
|
|
35
|
+
const fn = ((value) => options.validate(value));
|
|
36
|
+
const meta = {
|
|
37
|
+
emit: options.emit,
|
|
38
|
+
ruleName: options.name,
|
|
39
|
+
constraints: options.constraints ?? {},
|
|
40
|
+
};
|
|
41
|
+
if (options.requiresType !== undefined) {
|
|
42
|
+
meta.requiresType = options.requiresType;
|
|
43
|
+
}
|
|
44
|
+
if (options.isAsync !== undefined) {
|
|
45
|
+
meta.isAsync = options.isAsync;
|
|
46
|
+
}
|
|
47
|
+
if (options.plan !== undefined) {
|
|
48
|
+
meta.plan = options.plan;
|
|
49
|
+
}
|
|
50
|
+
defineRuleMetadata(fn, meta);
|
|
51
|
+
return fn;
|
|
52
|
+
}
|
|
53
|
+
function emitRulePlan(varName, ctx, ruleName, plan, cache, insideTypeGate) {
|
|
54
|
+
const failure = insideTypeGate ? stripSelfComparison(plan.failure) : plan.failure;
|
|
55
|
+
return `if (${emitPlanCheck(failure, varName, cache)}) ${ctx.fail(ruleName)};`;
|
|
56
|
+
}
|
|
57
|
+
/** Strip `x !== x` / `getTime() !== getTime()` self-comparison guards — redundant inside type gate */
|
|
58
|
+
function stripSelfComparison(check) {
|
|
59
|
+
if (check.kind === 'compare') {
|
|
60
|
+
return check;
|
|
61
|
+
}
|
|
62
|
+
const filtered = check.checks.filter(c => !isSelfComparison(c));
|
|
63
|
+
if (filtered.length === 0) {
|
|
64
|
+
return check;
|
|
65
|
+
} // safety: don't strip everything
|
|
66
|
+
if (filtered.length === 1) {
|
|
67
|
+
return filtered[0];
|
|
68
|
+
}
|
|
69
|
+
return { kind: check.kind, checks: filtered };
|
|
70
|
+
}
|
|
71
|
+
function isSelfComparison(check) {
|
|
72
|
+
if (check.kind !== 'compare' || check.op !== '!==') {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return exprEqual(check.left, check.right);
|
|
76
|
+
}
|
|
77
|
+
function exprEqual(a, b) {
|
|
78
|
+
if (a.kind !== b.kind) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
switch (a.kind) {
|
|
82
|
+
case 'value':
|
|
83
|
+
return true;
|
|
84
|
+
case 'literal':
|
|
85
|
+
return a.value === b.value;
|
|
86
|
+
case 'member':
|
|
87
|
+
return exprEqual(a.object, b.object);
|
|
88
|
+
case 'call0':
|
|
89
|
+
return a.method === b.method && exprEqual(a.object, b.object);
|
|
90
|
+
default:
|
|
91
|
+
// Compile-time exhaustiveness: adding a RulePlanExpr.kind without a case fails to compile here.
|
|
92
|
+
return a;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function emitPlanCheck(check, varName, cache) {
|
|
96
|
+
if (check.kind === 'compare') {
|
|
97
|
+
return `${emitPlanExpr(check.left, varName, cache)} ${check.op} ${emitPlanExpr(check.right, varName, cache)}`;
|
|
98
|
+
}
|
|
99
|
+
const joiner = check.kind === 'and' ? ' && ' : ' || ';
|
|
100
|
+
return `(${check.checks.map(part => emitPlanCheck(part, varName, cache)).join(joiner)})`;
|
|
101
|
+
}
|
|
102
|
+
function emitPlanExpr(expr, varName, cache) {
|
|
103
|
+
switch (expr.kind) {
|
|
104
|
+
case 'value':
|
|
105
|
+
return varName;
|
|
106
|
+
case 'literal':
|
|
107
|
+
return String(expr.value);
|
|
108
|
+
case 'member':
|
|
109
|
+
return cache?.length ?? `${emitPlanExpr(expr.object, varName, cache)}.length`;
|
|
110
|
+
case 'call0':
|
|
111
|
+
return cache?.time ?? `${emitPlanExpr(expr.object, varName, cache)}.getTime()`;
|
|
112
|
+
default:
|
|
113
|
+
// Compile-time exhaustiveness: adding a RulePlanExpr.kind without a case fails to compile here.
|
|
114
|
+
return expr;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export { planValue, planLength, planTime, planLiteral, planCompare, planOr, makePlannedRule, makeRule, emitRulePlan };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EmittableRule } from '../types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
declare function arrayContains(values: unknown[]): EmittableRule;
|
|
3
|
+
declare function arrayNotContains(values: unknown[]): EmittableRule;
|
|
4
|
+
declare function arrayMinSize(min: number): EmittableRule;
|
|
5
|
+
declare function arrayMaxSize(max: number): EmittableRule;
|
|
6
|
+
declare function arrayUnique(identifier?: (val: unknown) => unknown): EmittableRule;
|
|
7
|
+
declare const arrayNotEmpty: import("../types").InternalRule;
|
|
8
|
+
export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUnique, arrayNotEmpty };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { makePlannedRule, makeRule, planCompare, planLength } from '../rule-plan.js';
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// arrayContains(values) — array contains all specified values
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
function arrayContains(values) {
|
|
6
|
+
return makeRule({
|
|
7
|
+
name: 'arrayContains',
|
|
8
|
+
requiresType: 'array',
|
|
9
|
+
constraints: { values },
|
|
10
|
+
validate: value => Array.isArray(value) && values.every(v => value.includes(v)),
|
|
11
|
+
emit: (varName, ctx) => {
|
|
12
|
+
const i = ctx.addRef(values);
|
|
13
|
+
return `if (!refs[${i}].every(function(v){return ${varName}.includes(v);})) ${ctx.fail('arrayContains')};`;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// arrayNotContains(values) — array does not contain any of the specified values
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
function arrayNotContains(values) {
|
|
21
|
+
return makeRule({
|
|
22
|
+
name: 'arrayNotContains',
|
|
23
|
+
requiresType: 'array',
|
|
24
|
+
constraints: { values },
|
|
25
|
+
validate: value => Array.isArray(value) && values.every(v => !value.includes(v)),
|
|
26
|
+
emit: (varName, ctx) => {
|
|
27
|
+
const i = ctx.addRef(values);
|
|
28
|
+
return `if (refs[${i}].some(function(v){return ${varName}.includes(v);})) ${ctx.fail('arrayNotContains')};`;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// arrayMinSize(min) — minimum array length
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
function arrayMinSize(min) {
|
|
36
|
+
const plan = { cacheKey: 'length', failure: planCompare(planLength(), '<', min) };
|
|
37
|
+
return makePlannedRule({
|
|
38
|
+
name: 'arrayMinSize',
|
|
39
|
+
requiresType: 'array',
|
|
40
|
+
constraints: { min },
|
|
41
|
+
plan,
|
|
42
|
+
validate: value => Array.isArray(value) && value.length >= min,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
// arrayMaxSize(max) — maximum array length
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
function arrayMaxSize(max) {
|
|
49
|
+
const plan = { cacheKey: 'length', failure: planCompare(planLength(), '>', max) };
|
|
50
|
+
return makePlannedRule({
|
|
51
|
+
name: 'arrayMaxSize',
|
|
52
|
+
requiresType: 'array',
|
|
53
|
+
constraints: { max },
|
|
54
|
+
plan,
|
|
55
|
+
validate: value => Array.isArray(value) && value.length <= max,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
// arrayUnique(identifier?) — no duplicates in array
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
function arrayUnique(identifier) {
|
|
62
|
+
return makeRule({
|
|
63
|
+
name: 'arrayUnique',
|
|
64
|
+
requiresType: 'array',
|
|
65
|
+
constraints: {},
|
|
66
|
+
validate: value => {
|
|
67
|
+
if (!Array.isArray(value)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (identifier) {
|
|
71
|
+
const keys = value.map(identifier);
|
|
72
|
+
return new Set(keys).size === keys.length;
|
|
73
|
+
}
|
|
74
|
+
return new Set(value).size === value.length;
|
|
75
|
+
},
|
|
76
|
+
emit: (varName, ctx) => {
|
|
77
|
+
if (identifier) {
|
|
78
|
+
const i = ctx.addRef(identifier);
|
|
79
|
+
return `{var keys=${varName}.map(refs[${i}]);if(new Set(keys).size!==keys.length)${ctx.fail('arrayUnique')};}`;
|
|
80
|
+
}
|
|
81
|
+
return `if(new Set(${varName}).size!==${varName}.length)${ctx.fail('arrayUnique')};`;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// arrayNotEmpty — array is not empty (singleton)
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
const arrayNotEmptyPlan = { cacheKey: 'length', failure: planCompare(planLength(), '===', 0) };
|
|
89
|
+
const arrayNotEmpty = makePlannedRule({
|
|
90
|
+
name: 'arrayNotEmpty',
|
|
91
|
+
requiresType: 'array',
|
|
92
|
+
constraints: {},
|
|
93
|
+
plan: arrayNotEmptyPlan,
|
|
94
|
+
validate: value => Array.isArray(value) && value.length > 0,
|
|
95
|
+
});
|
|
96
|
+
export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUnique, arrayNotEmpty };
|