@bedrockio/yada 1.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/src/array.js ADDED
@@ -0,0 +1,131 @@
1
+ import Schema from './Schema';
2
+ import { ArrayError, ElementError, LocalizedError } from './errors';
3
+ import { wrapSchema } from './utils';
4
+
5
+ class ArraySchema extends Schema {
6
+ constructor(...args) {
7
+ let schemas, meta;
8
+
9
+ if (Array.isArray(args[0])) {
10
+ schemas = args[0];
11
+ meta = args[1];
12
+ } else {
13
+ schemas = args;
14
+ }
15
+
16
+ super({ message: 'Array failed validation.', ...meta, schemas });
17
+ this.setup();
18
+ }
19
+
20
+ setup() {
21
+ const { schemas } = this.meta;
22
+ const schema =
23
+ schemas.length > 1 ? new Schema().allow(schemas) : schemas[0];
24
+
25
+ this.assert('type', (val, options) => {
26
+ if (typeof val === 'string' && options.cast) {
27
+ val = val.split(',');
28
+ }
29
+ if (!Array.isArray(val)) {
30
+ throw new LocalizedError('Must be an array.');
31
+ }
32
+ return val;
33
+ });
34
+
35
+ if (schema) {
36
+ this.assert('elements', async (arr, options) => {
37
+ const errors = [];
38
+ const result = [];
39
+ for (let i = 0; i < arr.length; i++) {
40
+ const el = arr[i];
41
+ try {
42
+ result.push(await schema.validate(el, options));
43
+ } catch (error) {
44
+ if (error.details?.length === 1) {
45
+ errors.push(new ElementError(error.details[0].message, i));
46
+ } else {
47
+ errors.push(
48
+ new ElementError('Element failed validation.', i, error.details)
49
+ );
50
+ }
51
+ }
52
+ }
53
+ if (errors.length) {
54
+ throw new ArrayError(this.meta.message, errors);
55
+ } else {
56
+ return result;
57
+ }
58
+ });
59
+ }
60
+ }
61
+
62
+ min(length) {
63
+ return this.clone().assert('length', (arr) => {
64
+ if (arr.length < length) {
65
+ const s = length === 1 ? '' : 's';
66
+ throw new LocalizedError('Must contain at least {length} element{s}.', {
67
+ length,
68
+ s,
69
+ });
70
+ }
71
+ });
72
+ }
73
+
74
+ max(length) {
75
+ return this.clone().assert('length', (arr) => {
76
+ if (arr.length > length) {
77
+ const s = length === 1 ? '' : 's';
78
+ throw new LocalizedError(
79
+ 'Cannot contain more than {length} element{s}.',
80
+ {
81
+ length,
82
+ s,
83
+ }
84
+ );
85
+ }
86
+ });
87
+ }
88
+
89
+ latlng() {
90
+ return this.clone({ format: 'latlng' }).assert('format', (arr) => {
91
+ if (arr.length !== 2) {
92
+ throw new LocalizedError('Must be an array of length 2.');
93
+ } else {
94
+ const [lat, lng] = arr;
95
+ if (typeof lat !== 'number' || lat < -90 || lat > 90) {
96
+ throw new LocalizedError('Invalid latitude.');
97
+ } else if (typeof lng !== 'number' || lng < -180 || lng > 180) {
98
+ throw new LocalizedError('Invalid longitude.');
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ toString() {
105
+ return 'array';
106
+ }
107
+
108
+ toOpenApi(extra) {
109
+ let other;
110
+ const { schemas } = this.meta;
111
+ if (schemas.length > 1) {
112
+ other = {
113
+ oneOf: schemas.map((schema) => {
114
+ return schema.toOpenApi();
115
+ }),
116
+ };
117
+ } else if (schemas.length === 1) {
118
+ other = {
119
+ items: schemas[0].toOpenApi(),
120
+ };
121
+ }
122
+
123
+ return {
124
+ type: 'array',
125
+ ...super.toOpenApi(extra),
126
+ ...other,
127
+ };
128
+ }
129
+ }
130
+
131
+ export default wrapSchema(ArraySchema);
package/src/boolean.js ADDED
@@ -0,0 +1,25 @@
1
+ import TypeSchema from './TypeSchema';
2
+ import { LocalizedError } from './errors';
3
+ import { wrapSchema } from './utils';
4
+
5
+ class BooleanSchema extends TypeSchema {
6
+ constructor() {
7
+ super(Boolean);
8
+ this.assert('type', (val, options) => {
9
+ if (typeof val === 'string' && options.cast) {
10
+ const str = val.toLowerCase();
11
+ if (str === 'true' || str === '1') {
12
+ val = true;
13
+ } else if (str === 'false' || str === '0') {
14
+ val = false;
15
+ }
16
+ }
17
+ if (typeof val !== 'boolean') {
18
+ throw new LocalizedError('Must be a boolean.');
19
+ }
20
+ return val;
21
+ });
22
+ }
23
+ }
24
+
25
+ export default wrapSchema(BooleanSchema);
package/src/date.js ADDED
@@ -0,0 +1,130 @@
1
+ import validator from 'validator';
2
+ import { wrapSchema } from './utils';
3
+ import { LocalizedError } from './errors';
4
+
5
+ import Schema from './Schema';
6
+
7
+ class DateSchema extends Schema {
8
+ constructor() {
9
+ super();
10
+ this.assert('type', (val) => {
11
+ const date = new Date(val);
12
+ if ((!val && val !== 0) || isNaN(date.getTime())) {
13
+ throw new LocalizedError('Must be a valid date input.');
14
+ } else {
15
+ return date;
16
+ }
17
+ });
18
+ }
19
+
20
+ min(min) {
21
+ min = new Date(min);
22
+ return this.clone().assert('min', (date) => {
23
+ if (date < min) {
24
+ throw new LocalizedError('Must be after {date}.', {
25
+ date: min.toISOString(),
26
+ });
27
+ }
28
+ });
29
+ }
30
+
31
+ max(max) {
32
+ max = new Date(max);
33
+ return this.clone().assert('max', (date) => {
34
+ if (date > max) {
35
+ throw new LocalizedError('Must be before {date}.', {
36
+ date: max.toISOString(),
37
+ });
38
+ }
39
+ });
40
+ }
41
+
42
+ before(max) {
43
+ max = new Date(max);
44
+ return this.clone().assert('before', (date) => {
45
+ if (date >= max) {
46
+ throw new LocalizedError('Must be before {date}.', {
47
+ date: max.toISOString(),
48
+ });
49
+ }
50
+ });
51
+ }
52
+
53
+ after(min) {
54
+ min = new Date(min);
55
+ return this.clone().assert('after', (date) => {
56
+ if (date <= min) {
57
+ throw new LocalizedError('Must be after {date}.', {
58
+ date: min.toISOString(),
59
+ });
60
+ }
61
+ });
62
+ }
63
+
64
+ past() {
65
+ return this.clone().assert('past', (date) => {
66
+ const now = new Date();
67
+ if (date > now) {
68
+ throw new LocalizedError('Must be in the past.');
69
+ }
70
+ });
71
+ }
72
+
73
+ future() {
74
+ return this.clone().assert('future', (date) => {
75
+ const now = new Date();
76
+ if (date < now) {
77
+ throw new LocalizedError('Must be in the future.');
78
+ }
79
+ });
80
+ }
81
+
82
+ iso(format = 'date-time') {
83
+ return this.clone({ format }).assert('format', (val, options) => {
84
+ const { original } = options;
85
+ if (typeof original !== 'string') {
86
+ throw new LocalizedError('Must be a string.');
87
+ } else if (!validator.isISO8601(original)) {
88
+ throw new LocalizedError('Must be in ISO 8601 format.');
89
+ }
90
+ });
91
+ }
92
+
93
+ timestamp() {
94
+ return this.clone({ format: 'timestamp' }).assert(
95
+ 'format',
96
+ (date, { original }) => {
97
+ if (typeof original !== 'number') {
98
+ throw new LocalizedError('Must be a timestamp in milliseconds.');
99
+ }
100
+ }
101
+ );
102
+ }
103
+
104
+ unix() {
105
+ return this.clone({ format: 'unix timestamp' }).assert(
106
+ 'format',
107
+ (date, { original }) => {
108
+ if (typeof original !== 'number') {
109
+ throw new LocalizedError('Must be a timestamp in seconds.');
110
+ } else {
111
+ return new Date(original * 1000);
112
+ }
113
+ }
114
+ );
115
+ }
116
+
117
+ toString() {
118
+ return 'date';
119
+ }
120
+
121
+ toOpenApi(extra) {
122
+ const { format } = this.meta;
123
+ return {
124
+ type: format.includes('timestamp') ? 'number' : 'string',
125
+ ...super.toOpenApi(extra),
126
+ };
127
+ }
128
+ }
129
+
130
+ export default wrapSchema(DateSchema);
package/src/errors.js ADDED
@@ -0,0 +1,92 @@
1
+ import { getFullMessage } from './messages';
2
+ import { localize } from './localization';
3
+
4
+ export class LocalizedError extends Error {
5
+ constructor(message, values) {
6
+ super(localize(message, values));
7
+ }
8
+ }
9
+
10
+ export class ValidationError extends Error {
11
+ constructor(message, details = [], type = 'validation') {
12
+ super(localize(message));
13
+ this.details = details;
14
+ this.type = type;
15
+ }
16
+
17
+ toJSON() {
18
+ return {
19
+ type: this.type,
20
+ message: this.message,
21
+ details: this.details,
22
+ };
23
+ }
24
+
25
+ getFullMessage(options) {
26
+ return getFullMessage(this, {
27
+ delimiter: ' ',
28
+ ...options,
29
+ });
30
+ }
31
+ }
32
+
33
+ export class FieldError extends ValidationError {
34
+ constructor(message, field, original, details) {
35
+ super(message, details, 'field');
36
+ this.field = field;
37
+ this.original = original;
38
+ this.details = details;
39
+ }
40
+
41
+ toJSON() {
42
+ return {
43
+ field: this.field,
44
+ ...super.toJSON(),
45
+ };
46
+ }
47
+ }
48
+
49
+ export class ElementError extends ValidationError {
50
+ constructor(message, index, details) {
51
+ super(message, details, 'element');
52
+ this.index = index;
53
+ this.details = details;
54
+ }
55
+
56
+ toJSON() {
57
+ return {
58
+ index: this.index,
59
+ ...super.toJSON(),
60
+ };
61
+ }
62
+ }
63
+
64
+ export class AssertionError extends Error {
65
+ constructor(message, type, original) {
66
+ super(message);
67
+ this.type = type;
68
+ this.original = original;
69
+ }
70
+
71
+ toJSON() {
72
+ return {
73
+ type: this.type,
74
+ message: this.message,
75
+ };
76
+ }
77
+ }
78
+
79
+ export class ArrayError extends Error {
80
+ constructor(message, details) {
81
+ super(message);
82
+ this.details = details;
83
+ }
84
+ }
85
+
86
+ export function isSchemaError(arg) {
87
+ return (
88
+ arg instanceof ValidationError ||
89
+ arg instanceof AssertionError ||
90
+ arg instanceof ArrayError
91
+ );
92
+ }
package/src/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import array from './array';
2
+ import boolean from './boolean';
3
+ import date from './date';
4
+ import number from './number';
5
+ import object from './object';
6
+ import string from './string';
7
+
8
+ import { wrapArgs, wrapAny, isSchema, isSchemaError } from './utils';
9
+ import { useLocalizer, getLocalizerTemplates } from './localization';
10
+ import { LocalizedError } from './errors';
11
+
12
+ const allow = wrapArgs('allow');
13
+ const reject = wrapArgs('reject');
14
+ const custom = wrapArgs('custom');
15
+ const any = wrapAny();
16
+
17
+ export default {
18
+ array,
19
+ boolean,
20
+ date,
21
+ number,
22
+ object,
23
+ string,
24
+ any,
25
+ allow,
26
+ reject,
27
+ custom,
28
+ isSchema,
29
+ isSchemaError,
30
+ useLocalizer,
31
+ getLocalizerTemplates,
32
+ LocalizedError,
33
+ };
34
+
35
+ export {
36
+ array,
37
+ boolean,
38
+ date,
39
+ number,
40
+ object,
41
+ string,
42
+ any,
43
+ allow,
44
+ reject,
45
+ custom,
46
+ isSchema,
47
+ isSchemaError,
48
+ useLocalizer,
49
+ getLocalizerTemplates,
50
+ LocalizedError,
51
+ };
@@ -0,0 +1,38 @@
1
+ const TOKEN_REG = /{(.+?)}/g;
2
+
3
+ let localizer;
4
+ let templates = {};
5
+
6
+ export function useLocalizer(arg) {
7
+ const fn = typeof arg === 'function' ? arg : (template) => arg[template];
8
+ localizer = fn;
9
+ templates = {};
10
+ }
11
+
12
+ export function getLocalized(template) {
13
+ if (localizer) {
14
+ return localizer(template);
15
+ }
16
+ }
17
+
18
+ export function localize(template, values = {}) {
19
+ let message = template;
20
+ if (localizer) {
21
+ let localized = getLocalized(template);
22
+ if (typeof localized === 'function') {
23
+ localized = localized(values);
24
+ }
25
+ if (localized) {
26
+ message = localized;
27
+ }
28
+ }
29
+ templates[template] = message;
30
+
31
+ return message.replace(TOKEN_REG, (match, token) => {
32
+ return values[token];
33
+ });
34
+ }
35
+
36
+ export function getLocalizerTemplates() {
37
+ return templates;
38
+ }
@@ -0,0 +1,72 @@
1
+ import { isSchemaError } from './errors';
2
+ import { localize } from './localization';
3
+
4
+ export function getFullMessage(error, options) {
5
+ const { delimiter } = options;
6
+ if (error.details) {
7
+ return error.details
8
+ .map((error) => {
9
+ if (isSchemaError(error)) {
10
+ return getFullMessage(error, {
11
+ field: error.field,
12
+ index: error.index,
13
+ ...options,
14
+ });
15
+ } else {
16
+ return error.message;
17
+ }
18
+ })
19
+ .join(delimiter);
20
+ } else {
21
+ return getLabeledMessage(error, options);
22
+ }
23
+ }
24
+
25
+ function getLabeledMessage(error, options) {
26
+ const { field, index } = options;
27
+ const base = getBase(error.message);
28
+ if (field) {
29
+ const msg = `{field} ${base}`;
30
+ return localize(msg, {
31
+ field: getFieldLabel(field, options),
32
+ });
33
+ } else if (index != null) {
34
+ const msg = `Element at index "{index}" ${base}`;
35
+ return localize(msg, {
36
+ index,
37
+ });
38
+ } else {
39
+ return localize(base);
40
+ }
41
+ }
42
+
43
+ function getFieldLabel(field, options) {
44
+ const { natural } = options;
45
+ if (natural) {
46
+ return naturalize(field);
47
+ } else {
48
+ return `"${field}"`;
49
+ }
50
+ }
51
+
52
+ function getBase(str) {
53
+ if (str === 'Value is required.') {
54
+ return 'is required.';
55
+ } else {
56
+ return downcase(str);
57
+ }
58
+ }
59
+
60
+ function naturalize(str) {
61
+ const first = str.slice(0, 1).toUpperCase();
62
+ let rest = str.slice(1);
63
+ rest = rest.replace(/[A-Z]+/, (caps) => {
64
+ return ' ' + caps.toLowerCase();
65
+ });
66
+ rest = rest.replace(/[-_]/g, ' ');
67
+ return first + rest;
68
+ }
69
+
70
+ function downcase(str) {
71
+ return str.slice(0, 1).toLowerCase() + str.slice(1);
72
+ }
package/src/number.js ADDED
@@ -0,0 +1,84 @@
1
+ import TypeSchema from './TypeSchema';
2
+ import { LocalizedError } from './errors';
3
+ import { wrapSchema } from './utils';
4
+
5
+ class NumberSchema extends TypeSchema {
6
+ constructor() {
7
+ super(Number);
8
+ this.assert('type', (val, options) => {
9
+ if (typeof val === 'string' && options.cast) {
10
+ val = Number(val);
11
+ }
12
+ if (typeof val !== 'number' || isNaN(val)) {
13
+ throw new LocalizedError('Must be a number.');
14
+ }
15
+ return val;
16
+ });
17
+ }
18
+
19
+ min(min, msg) {
20
+ msg ||= 'Must be greater than {min}.';
21
+ return this.clone({ min }).assert('min', (num) => {
22
+ if (num < min) {
23
+ throw new LocalizedError(msg, {
24
+ min,
25
+ });
26
+ }
27
+ });
28
+ }
29
+
30
+ max(max, msg) {
31
+ msg ||= 'Must be less than {max}.';
32
+ return this.clone({ max }).assert('max', (num) => {
33
+ if (num > max) {
34
+ throw new LocalizedError(msg, {
35
+ max,
36
+ });
37
+ }
38
+ });
39
+ }
40
+
41
+ negative() {
42
+ return this.max(0, 'Must be negative.');
43
+ }
44
+
45
+ positive() {
46
+ return this.min(0, 'Must be positive.');
47
+ }
48
+
49
+ integer() {
50
+ return this.clone().assert('integer', (num) => {
51
+ if (!Number.isInteger(num)) {
52
+ throw new LocalizedError('Must be an integer.');
53
+ }
54
+ });
55
+ }
56
+
57
+ multiple(multiple) {
58
+ return this.clone({ multiple }).assert('multiple', (num) => {
59
+ if (num % multiple !== 0) {
60
+ throw new LocalizedError('Must be a multiple of {multiple}.', {
61
+ multiple,
62
+ });
63
+ }
64
+ });
65
+ }
66
+
67
+ toOpenApi(extra) {
68
+ const { min, max, multiple } = this.meta;
69
+ return {
70
+ ...super.toOpenApi(extra),
71
+ ...(min != null && {
72
+ minimum: min,
73
+ }),
74
+ ...(max != null && {
75
+ maximum: max,
76
+ }),
77
+ ...(multiple != null && {
78
+ multipleOf: multiple,
79
+ }),
80
+ };
81
+ }
82
+ }
83
+
84
+ export default wrapSchema(NumberSchema);