pema 0.9.2 → 0.11.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.
@@ -12,6 +12,7 @@ export default abstract class PrimitiveType<StaticType, Name extends string> ext
12
12
  get options(): ParseOptions<unknown>;
13
13
  get validators(): Validator<StaticType>[];
14
14
  with(next: Next<StaticType>): this;
15
+ check(predicate: (value: StaticType) => boolean, message: string): this;
15
16
  parse(u: unknown, options?: ParseOptions<StaticType>): Infer<this>;
16
17
  }
17
18
  export {};
@@ -24,6 +24,14 @@ export default class PrimitiveType extends Type {
24
24
  const Constructor = this.constructor;
25
25
  return new Constructor([...this.#validators, ...next.validators ?? []], { ...this.#options, ...next.options ?? {} });
26
26
  }
27
+ check(predicate, message) {
28
+ return this.with({
29
+ validators: [value => {
30
+ if (!predicate(value))
31
+ throw E.invalid_format(value, message);
32
+ }],
33
+ });
34
+ }
27
35
  parse(u, options = {}) {
28
36
  const x = resolve(u);
29
37
  // hotpath: avoid object spread when possible
@@ -1,19 +1,36 @@
1
+ import type Infer from "#Infer";
2
+ import type ParseOptions from "#ParseOptions";
1
3
  import PrimitiveType from "#PrimitiveType";
2
4
  import Storable from "#Storable";
5
+ import type MessageOptions from "#validator/MessageOptions";
6
+ type Transform = (value: string) => string;
7
+ type Next = {
8
+ options?: ParseOptions;
9
+ transforms?: Transform[];
10
+ validators?: ((value: string) => void)[];
11
+ };
3
12
  export default class StringType extends PrimitiveType<string, "StringType"> implements Storable<"string"> {
13
+ #private;
14
+ constructor(validators?: ((value: string) => void)[], options?: ParseOptions, transforms?: Transform[]);
4
15
  get name(): "string";
5
16
  get datatype(): "string";
6
- isotime(): this;
7
- regex(pattern: RegExp): this;
8
- email(): this;
9
- startsWith(prefix: string): this;
10
- endsWith(suffix: string): this;
11
- min(limit: number): this;
12
- max(limit: number): this;
13
- length(from: number, to: number): this;
17
+ with(next: Next): this;
18
+ parse(u: unknown, options?: ParseOptions<string>): Infer<this>;
19
+ trim(): this;
20
+ lowercase(): this;
21
+ uppercase(): this;
22
+ isotime(options?: MessageOptions<string>): this;
23
+ regex(pattern: RegExp, options?: MessageOptions<string>): this;
24
+ email(options?: MessageOptions<string>): this;
25
+ startsWith(prefix: string, options?: MessageOptions<string>): this;
26
+ endsWith(suffix: string, options?: MessageOptions<string>): this;
27
+ min(limit: number, options?: MessageOptions<string>): this;
28
+ max(limit: number, options?: MessageOptions<string>): this;
29
+ length(from: number, to: number, options?: MessageOptions<string>): this;
14
30
  toJSON(): {
15
31
  type: "string";
16
32
  datatype: "string";
17
33
  };
18
34
  }
35
+ export {};
19
36
  //# sourceMappingURL=StringType.d.ts.map
@@ -1,5 +1,11 @@
1
+ import CoerceKey from "#CoerceKey";
2
+ import E from "#errors";
3
+ import Loose from "#Loose";
4
+ import ParsedKey from "#ParsedKey";
5
+ import ParseError from "#ParseError";
1
6
  import PrimitiveType from "#PrimitiveType";
2
- import E from "#schema-errors";
7
+ import resolve from "#resolve";
8
+ import S from "#schema-errors";
3
9
  import Storable from "#Storable";
4
10
  import email from "#validator/email";
5
11
  import ends_with from "#validator/ends-with";
@@ -9,40 +15,99 @@ import max from "#validator/max";
9
15
  import min from "#validator/min";
10
16
  import regex from "#validator/regex";
11
17
  import starts_with from "#validator/starts-with";
18
+ import is from "@rcompat/is";
12
19
  export default class StringType extends PrimitiveType {
20
+ #transforms;
21
+ constructor(validators = [], options = {}, transforms = []) {
22
+ super(validators, options);
23
+ this.#transforms = transforms;
24
+ }
13
25
  get name() {
14
26
  return "string";
15
27
  }
16
28
  get datatype() {
17
29
  return "string";
18
30
  }
19
- isotime() {
20
- return this.with({ validators: [isotime] });
31
+ with(next) {
32
+ return new StringType([...this.validators, ...next.validators ?? []], { ...this.options, ...next.options ?? {} }, [...this.#transforms, ...next.transforms ?? []]);
33
+ }
34
+ parse(u, options = {}) {
35
+ const x = resolve(u);
36
+ const has_instance_options = is.defined(this.options[ParsedKey]);
37
+ const $options = has_instance_options
38
+ ? { ...this.options, ...options }
39
+ : options;
40
+ const loose = this[Loose] ?? $options[Loose] ?? false;
41
+ const $x = loose ? this[CoerceKey](x) : x;
42
+ if (typeof $x !== this.name)
43
+ throw E.invalid_type($x, this.name, $options);
44
+ let value = $x;
45
+ for (const transform of this.#transforms) {
46
+ value = transform(value);
47
+ }
48
+ const option_validators = $options.validators;
49
+ const validators = is.array(option_validators)
50
+ ? option_validators.concat(this.validators)
51
+ : this.validators;
52
+ const base = $options[ParsedKey] ?? "";
53
+ for (let i = 0; i < validators.length; i++) {
54
+ try {
55
+ validators[i](value);
56
+ }
57
+ catch (e) {
58
+ if (ParseError.is(e)) {
59
+ const rebased = e.issues.map(issue => ({
60
+ ...issue,
61
+ path: issue.path === ""
62
+ ? base
63
+ : (base === "" ? issue.path : (base + issue.path)),
64
+ }));
65
+ throw new ParseError(rebased);
66
+ }
67
+ const message = is.error(e) && is.string(e.message)
68
+ ? e.message
69
+ : String(e);
70
+ throw E.invalid_type(x, message, base);
71
+ }
72
+ }
73
+ return value;
74
+ }
75
+ trim() {
76
+ return this.with({ transforms: [value => value.trim()] });
77
+ }
78
+ lowercase() {
79
+ return this.with({ transforms: [value => value.toLowerCase()] });
80
+ }
81
+ uppercase() {
82
+ return this.with({ transforms: [value => value.toUpperCase()] });
83
+ }
84
+ isotime(options) {
85
+ return this.with({ validators: [isotime(options)] });
21
86
  }
22
- regex(pattern) {
23
- return this.with({ validators: [regex(pattern)] });
87
+ regex(pattern, options) {
88
+ return this.with({ validators: [regex(pattern, options)] });
24
89
  }
25
- email() {
26
- return this.with({ validators: [email] });
90
+ email(options) {
91
+ return this.with({ validators: [email(options)] });
27
92
  }
28
- startsWith(prefix) {
29
- return this.with({ validators: [starts_with(prefix)] });
93
+ startsWith(prefix, options) {
94
+ return this.with({ validators: [starts_with(prefix, options)] });
30
95
  }
31
- endsWith(suffix) {
32
- return this.with({ validators: [ends_with(suffix)] });
96
+ endsWith(suffix, options) {
97
+ return this.with({ validators: [ends_with(suffix, options)] });
33
98
  }
34
- min(limit) {
99
+ min(limit, options) {
35
100
  if (limit < 0)
36
- throw E.min_negative(limit);
37
- return this.with({ validators: [min(limit)] });
101
+ throw S.min_negative(limit);
102
+ return this.with({ validators: [min(limit, options)] });
38
103
  }
39
- max(limit) {
104
+ max(limit, options) {
40
105
  if (limit < 0)
41
- throw E.max_negative(limit);
42
- return this.with({ validators: [max(limit)] });
106
+ throw S.max_negative(limit);
107
+ return this.with({ validators: [max(limit, options)] });
43
108
  }
44
- length(from, to) {
45
- return this.with({ validators: [length(from, to)] });
109
+ length(from, to, options) {
110
+ return this.with({ validators: [length(from, to, options)] });
46
111
  }
47
112
  toJSON() {
48
113
  return Storable.serialize(this);
@@ -16,12 +16,14 @@ export default function normalize(x) {
16
16
  return new UndefinedType();
17
17
  if (is.string(x) || is.number(x) || is.boolean(x))
18
18
  return new LiteralType(x);
19
- if (is.newable(x))
19
+ if (is.newable(x)) {
20
20
  return new ConstructorType(x);
21
+ }
21
22
  if (Array.isArray(x)) {
22
- return x.length === 1
23
- ? new ArrayType(normalize(x[0]))
24
- : new TupleType(x.map(normalize));
23
+ const schemas = x;
24
+ return schemas.length === 1
25
+ ? new ArrayType(normalize(schemas[0]))
26
+ : new TupleType(schemas.map(normalize));
25
27
  }
26
28
  if (is.dict(x)) {
27
29
  const props = {};
@@ -29,6 +31,6 @@ export default function normalize(x) {
29
31
  props[k] = normalize(v);
30
32
  return new ObjectType(props);
31
33
  }
32
- throw new TypeError("Unsupported type-like value passed to asType");
34
+ throw new TypeError("Unsupported type-like value passed to normalize");
33
35
  }
34
36
  //# sourceMappingURL=normalize.js.map
@@ -0,0 +1,6 @@
1
+ type Message<T> = string | ((value: T) => string);
2
+ type MessageOptions<T> = Message<T> | {
3
+ message?: Message<T>;
4
+ };
5
+ export type { MessageOptions as default };
6
+ //# sourceMappingURL=MessageOptions.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MessageOptions.js.map
@@ -1,3 +1,3 @@
1
- declare const _default: import("../Validator.js").default<string>;
2
- export default _default;
1
+ import type MessageOptions from "#validator/MessageOptions";
2
+ export default function email(options?: MessageOptions<string>): import("../Validator.js").default<string>;
3
3
  //# sourceMappingURL=email.d.ts.map
@@ -1,4 +1,6 @@
1
1
  import regex from "#validator/regex";
2
- const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
3
- export default regex(email, x => `"${x}" is not a valid email`);
2
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
3
+ export default function email(options) {
4
+ return regex(re, options ?? (x => `"${x}" is not a valid email`));
5
+ }
4
6
  //# sourceMappingURL=email.js.map
@@ -1,3 +1,4 @@
1
1
  import type Validator from "#Validator";
2
- export default function ends_with(suffix: string): Validator<string>;
2
+ import type MessageOptions from "#validator/MessageOptions";
3
+ export default function ends_with(suffix: string, options?: MessageOptions<string>): Validator<string>;
3
4
  //# sourceMappingURL=ends-with.d.ts.map
@@ -1,9 +1,10 @@
1
1
  import E from "#errors";
2
- export default function ends_with(suffix) {
2
+ import message from "#validator/message";
3
+ export default function ends_with(suffix, options) {
4
+ const format = message(options, x => `"${x}" does not end with "${suffix}"`);
3
5
  return (x) => {
4
- if (!x.endsWith(suffix)) {
5
- throw E.invalid_format(x, `"${x}" does not end with "${suffix}"`);
6
- }
6
+ if (!x.endsWith(suffix))
7
+ throw E.invalid_format(x, format(x));
7
8
  };
8
9
  }
9
10
  //# sourceMappingURL=ends-with.js.map
@@ -1,6 +1,7 @@
1
1
  import E from "#errors";
2
+ import is from "@rcompat/is";
2
3
  export default function f32(x) {
3
- if (x !== new Float32Array([x])[0])
4
+ if (!is.f32(x))
4
5
  throw E.out_of_range(x, `${x} is not a 32-bit float`);
5
6
  }
6
7
  //# sourceMappingURL=f32.js.map
@@ -1,2 +1,3 @@
1
- export default function isotime(x: string): void;
1
+ import type MessageOptions from "#validator/MessageOptions";
2
+ export default function isotime(options?: MessageOptions<string>): (x: string) => void;
2
3
  //# sourceMappingURL=isotime.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import E from "#errors";
2
+ import message from "#validator/message";
2
3
  import regex from "#validator/regex";
3
4
  const ISOTIME = /^T?(?<hour>\d{2}):?(?<minute>\d{2}):?(?<second>\d{2})$/u;
4
5
  const RANGE = {
@@ -6,23 +7,24 @@ const RANGE = {
6
7
  minute: { max: 59, min: 0 },
7
8
  second: { max: 60, min: 0 },
8
9
  };
9
- function validator(s) {
10
- return `"${s}" is not a valid ISO time`;
11
- }
12
- export default function isotime(x) {
13
- // check format
14
- regex(ISOTIME, validator)(x);
15
- // check range
16
- const match = ISOTIME.exec(x);
17
- const g = match.groups;
18
- const h = Number(g.hour);
19
- const m = Number(g.minute);
20
- const s = Number(g.second);
21
- const in_range = h >= RANGE.hour.min && h <= RANGE.hour.max &&
22
- m >= RANGE.minute.min && m <= RANGE.minute.max &&
23
- s >= RANGE.second.min && s <= RANGE.second.max;
24
- if (!in_range)
25
- throw E.out_of_range(x, validator(x));
10
+ const fallback = (s) => `"${s}" is not a valid ISO time`;
11
+ export default function isotime(options) {
12
+ const format = message(options, fallback);
13
+ return (x) => {
14
+ // check format
15
+ regex(ISOTIME, format)(x);
16
+ // check range
17
+ const match = ISOTIME.exec(x);
18
+ const g = match.groups;
19
+ const h = Number(g.hour);
20
+ const m = Number(g.minute);
21
+ const s = Number(g.second);
22
+ const in_range = h >= RANGE.hour.min && h <= RANGE.hour.max &&
23
+ m >= RANGE.minute.min && m <= RANGE.minute.max &&
24
+ s >= RANGE.second.min && s <= RANGE.second.max;
25
+ if (!in_range)
26
+ throw E.out_of_range(x, format(x));
27
+ };
26
28
  }
27
29
  ;
28
30
  //# sourceMappingURL=isotime.js.map
@@ -1,5 +1,6 @@
1
1
  import type Validator from "#Validator";
2
+ import type MessageOptions from "#validator/MessageOptions";
2
3
  type Input = string | unknown[];
3
- export default function length(from: number, to: number): Validator<Input>;
4
+ export default function length<I extends Input>(from: number, to: number, options?: MessageOptions<I>): Validator<I>;
4
5
  export {};
5
6
  //# sourceMappingURL=length.d.ts.map
@@ -1,20 +1,21 @@
1
1
  import E from "#errors";
2
2
  import S from "#schema-errors";
3
+ import message from "#validator/message";
3
4
  import is from "@rcompat/is";
4
- export default function length(from, to) {
5
+ export default function length(from, to, options) {
5
6
  if (!is.finite(from) || !is.finite(to))
6
7
  throw S.length_not_finite(from, to);
7
8
  if (from < 0 || to < 0)
8
9
  throw S.length_not_positive(from, to);
9
10
  if (from > to)
10
11
  throw S.length_from_exceeds_to(from, to);
12
+ const format = message(options, () => "length out of range");
11
13
  return (x) => {
12
14
  if (!is.string(x) && !is.array(x)) {
13
15
  throw E.invalid_type(x, "string or array");
14
16
  }
15
- if (x.length < from || x.length > to) {
16
- throw E.out_of_range(x, "length out of range");
17
- }
17
+ if (x.length < from || x.length > to)
18
+ throw E.out_of_range(x, format(x));
18
19
  };
19
20
  }
20
21
  ;
@@ -1,6 +1,7 @@
1
1
  import type Validator from "#Validator";
2
+ import type MessageOptions from "#validator/MessageOptions";
2
3
  type Limit = bigint | number;
3
4
  type Input = bigint | number | string | unknown[];
4
- export default function max(limit: Limit): Validator<Input>;
5
+ export default function max<I extends Input>(limit: Limit, options?: MessageOptions<I>): Validator<I>;
5
6
  export {};
6
7
  //# sourceMappingURL=max.d.ts.map
@@ -1,7 +1,14 @@
1
1
  import E from "#errors";
2
2
  import S from "#schema-errors";
3
+ import message from "#validator/message";
3
4
  import is from "@rcompat/is";
4
- export default function max(limit) {
5
+ export default function max(limit, options) {
6
+ const format = message(options, x => {
7
+ if (is.number(x) || is.bigint(x))
8
+ return `${x} is greater than ${limit}`;
9
+ const unit = is.string(x) ? "characters" : "items";
10
+ return `max ${limit} ${unit}`;
11
+ });
5
12
  // validate limit once
6
13
  if (is.number(limit)) {
7
14
  if (!is.finite(limit))
@@ -9,13 +16,11 @@ export default function max(limit) {
9
16
  return (x) => {
10
17
  if (is.number(x)) {
11
18
  if (x > limit)
12
- throw E.too_large(x, `${x} is greater than ${limit}`);
19
+ throw E.too_large(x, format(x));
13
20
  }
14
21
  else if (is.string(x) || is.array(x)) {
15
- if (x.length > limit) {
16
- const unit = is.string(x) ? "characters" : "items";
17
- throw E.too_large(x, `max ${limit} ${unit}`);
18
- }
22
+ if (x.length > limit)
23
+ throw E.too_large(x, format(x));
19
24
  }
20
25
  else {
21
26
  throw E.invalid_type(x, "number");
@@ -26,7 +31,7 @@ export default function max(limit) {
26
31
  return (x) => {
27
32
  if (is.bigint(x)) {
28
33
  if (x > limit)
29
- throw E.too_large(x, `${x} is greater than ${limit}`);
34
+ throw E.too_large(x, format(x));
30
35
  }
31
36
  else {
32
37
  throw E.invalid_type(x, "bigint");
@@ -0,0 +1,3 @@
1
+ import type MessageOptions from "#validator/MessageOptions";
2
+ export default function message<T>(options: MessageOptions<T> | undefined, fallback: (value: T) => string): (value: T) => any;
3
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1,8 @@
1
+ import is from "@rcompat/is";
2
+ export default function message(options, fallback) {
3
+ const option = is.dict(options) ? options.message : options;
4
+ return (value) => typeof option === "function"
5
+ ? option(value)
6
+ : option ?? fallback(value);
7
+ }
8
+ //# sourceMappingURL=message.js.map
@@ -1,6 +1,7 @@
1
1
  import type Validator from "#Validator";
2
+ import type MessageOptions from "#validator/MessageOptions";
2
3
  type Limit = bigint | number;
3
4
  type Input = bigint | number | string | unknown[];
4
- export default function min(limit: Limit): Validator<Input>;
5
+ export default function min<I extends Input>(limit: Limit, options?: MessageOptions<I>): Validator<I>;
5
6
  export {};
6
7
  //# sourceMappingURL=min.d.ts.map
@@ -1,7 +1,13 @@
1
1
  import E from "#errors";
2
2
  import S from "#schema-errors";
3
+ import message from "#validator/message";
3
4
  import is from "@rcompat/is";
4
- export default function min(limit) {
5
+ export default function min(limit, options) {
6
+ const format = message(options, x => {
7
+ if (is.number(x) || is.bigint(x))
8
+ return `${x} is lower than ${limit}`;
9
+ return `min ${limit} ${is.string(x) ? "characters" : "items"}`;
10
+ });
5
11
  // validate limit once
6
12
  if (is.number(limit)) {
7
13
  if (!is.finite(limit))
@@ -9,12 +15,11 @@ export default function min(limit) {
9
15
  return (x) => {
10
16
  if (is.number(x)) {
11
17
  if (x < limit)
12
- throw E.too_small(x, `${x} is lower than ${limit}`);
18
+ throw E.too_small(x, format(x));
13
19
  }
14
20
  else if (is.string(x) || is.array(x)) {
15
21
  if (x.length < limit) {
16
- const unit = is.string(x) ? "characters" : "items";
17
- throw E.too_small(x, `min ${limit} ${unit}`);
22
+ throw E.too_small(x, format(x));
18
23
  }
19
24
  }
20
25
  else {
@@ -26,7 +31,7 @@ export default function min(limit) {
26
31
  return (x) => {
27
32
  if (is.bigint(x)) {
28
33
  if (x < limit)
29
- throw E.too_small(x, `${x} is lower than ${limit}`);
34
+ throw E.too_small(x, format(x));
30
35
  }
31
36
  else {
32
37
  throw E.invalid_type(x, "bigint");
@@ -1,5 +1,4 @@
1
1
  import type Validator from "#Validator";
2
- type ErrorFunction = (x: string) => string;
3
- export default function regex(format: RegExp, error?: ErrorFunction): Validator<string>;
4
- export {};
2
+ import type MessageOptions from "#validator/MessageOptions";
3
+ export default function regex(re: RegExp, options?: MessageOptions<string>): Validator<string>;
5
4
  //# sourceMappingURL=regex.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import E from "#errors";
2
- export default function regex(format, error) {
2
+ import message from "#validator/message";
3
+ export default function regex(re, options) {
4
+ const format = message(options, y => `"${y}" is not a valid ${String(re)}`);
3
5
  return (x) => {
4
- if (!format.test(x)) {
5
- const message = (error ?? ((y) => `"${y}" is not a valid ${String(format)}`))(x);
6
- throw E.invalid_format(x, message);
7
- }
6
+ if (!re.test(x))
7
+ throw E.invalid_format(x, format(x));
8
8
  };
9
9
  }
10
10
  ;
@@ -1,3 +1,4 @@
1
1
  import type Validator from "#Validator";
2
- export default function starts_with(prefix: string): Validator<string>;
2
+ import type MessageOptions from "#validator/MessageOptions";
3
+ export default function starts_with(prefix: string, options?: MessageOptions<string>): Validator<string>;
3
4
  //# sourceMappingURL=starts-with.d.ts.map
@@ -1,9 +1,10 @@
1
1
  import E from "#errors";
2
- export default function starts_with(prefix) {
2
+ import message from "#validator/message";
3
+ export default function starts_with(prefix, options) {
4
+ const format = message(options, x => `"${x}" does not start with "${prefix}"`);
3
5
  return (x) => {
4
- if (!x.startsWith(prefix)) {
5
- throw E.invalid_format(x, `"${x}" does not start with "${prefix}"`);
6
- }
6
+ if (!x.startsWith(prefix))
7
+ throw E.invalid_format(x, format(x));
7
8
  };
8
9
  }
9
10
  //# sourceMappingURL=starts-with.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pema",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "Primate schema validation",
5
5
  "homepage": "https://primate.run/docs/validation",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",
@@ -18,9 +18,9 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@rcompat/assert": "^0.14.0",
21
- "@rcompat/cli": "^0.25.0",
21
+ "@rcompat/cli": "^0.25.1",
22
22
  "@rcompat/error": "^0.9.0",
23
- "@rcompat/is": "^0.12.0",
23
+ "@rcompat/is": "^0.12.2",
24
24
  "@rcompat/symbol": "^0.3.0"
25
25
  },
26
26
  "devDependencies": {
@@ -43,7 +43,7 @@
43
43
  }
44
44
  },
45
45
  "scripts": {
46
- "build": "npm run clean && tsc",
46
+ "build": "pnpm run clean && tsc",
47
47
  "clean": "rm -rf ./lib",
48
48
  "lint": "eslint .",
49
49
  "lint:fix": "eslint . --fix",