Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 19f33c6

Browse files
committedMay 4, 2023
Initial commit
0 parents  commit 19f33c6

17 files changed

+3616
-0
lines changed
 

‎.editorconfig

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
indent_style = space
8+
indent_size = 2
9+
trim_trailing_whitespace = true
10+
11+
[*.md]
12+
trim_trailing_whitespace = false
13+
indent_size = 4

‎.eslintrc.cjs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
es2021: true,
5+
},
6+
extends: 'eslint:recommended',
7+
overrides: [],
8+
parserOptions: {
9+
ecmaVersion: 'latest',
10+
sourceType: 'module',
11+
},
12+
rules: {},
13+
};

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

‎.prettierrc.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"printWidth": 160,
3+
"tabWidth": 2,
4+
"singleQuote": true
5+
}

‎LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) Mohd Hafizuddin M Marzuki <hafizuddin_83@yahoo.com>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

‎README.md

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Quival
2+
3+
![npm](https://img.shields.io/npm/v/quival?style=flat-square)
4+
![npm](https://img.shields.io/npm/dt/quival?style=flat-square)
5+
![NPM](https://img.shields.io/npm/l/quival?style=flat-square)
6+
7+
This library provides the ability to perform data validation easily in JavaScript. It is heavily based on [Laravel Validation](https://laravel.com/docs/10.x/validation).
8+
9+
By sharing similar conventions, it is possible to reuse validation rules on both front and back ends. This library can be used for preliminary data validation on the front end, before submitting the data to Laravel-based app, where the data should be validated again.
10+
11+
Why is there a need to perform validation on the front end? Early data validation allows an app to show errors to users immediately, before the data is even submitted. Live feedback is beneficial and can improve user experience.
12+
13+
This library is intended to be used in the browser environment, and it was not tested in other enviroments such as Node.
14+
15+
## Installation
16+
17+
```bash
18+
npm install quival
19+
```
20+
21+
## Features
22+
23+
- Provide similar conventions to Laravel Validation
24+
- Implement most of the rules listed [here](https://laravel.com/docs/10.x/validation#available-validation-rules)
25+
26+
## Example
27+
28+
The code snippet below demonstrates the usage of `Validator` class.
29+
30+
```js
31+
import { Validator } from 'quival';
32+
import enLocale from 'quival/src/locales/en.js';
33+
34+
// Set localization messages
35+
Validator.setLocale('en');
36+
Validator.setMessages('en', enLocale);
37+
38+
// Register custom checker
39+
Validator.addChecker('phone_number', (attribute, value, parameters) => {
40+
return !/[^0-9\+\-\s]/.test(value);
41+
}, 'The :attribute field must be a valid phone number.');
42+
43+
// Prepare arguments
44+
const data = {
45+
username: 'idea💡',
46+
name: '',
47+
email: 'test',
48+
phone_number: 'test',
49+
letters: ['a', 'b', 'B', 'a'],
50+
items: {
51+
x: 'a',
52+
y: null,
53+
z: 12,
54+
},
55+
payment_type: 'cc',
56+
card_number: '',
57+
};
58+
59+
const rules = {
60+
username: ['required', 'ascii', 'min:3'],
61+
name: ['required', 'min:3'],
62+
email: ['required', 'email'],
63+
phone_number: ['required', 'phone_number'],
64+
'letters.*': ['distinct:ignore_case'],
65+
items: ['array', 'size:5'],
66+
'items.*': ['required', 'string'],
67+
payment_type: ['required', 'in:cc,paypal'],
68+
cc_number: ['required_if:payment_type,cc'],
69+
};
70+
71+
const customMessages = {
72+
'items.size': 'The size of the array must be equal to :size items only.',
73+
};
74+
75+
const customAttributes = {
76+
cc_number: 'Credit Card Number',
77+
};
78+
79+
const customValues = {
80+
payment_type: {
81+
cc: 'credit card',
82+
},
83+
};
84+
85+
// Create Validator instance
86+
const validator = new Validator(data, rules, customMessages, customAttributes, customValues);
87+
88+
// Perform validation
89+
validator
90+
.validate()
91+
.then(() => {
92+
console.log('Successful!');
93+
})
94+
.catch((messages) => {
95+
if (messages instanceof Error) {
96+
throw messages;
97+
}
98+
99+
console.log(messages);
100+
});
101+
```
102+
103+
The produced error messages for the code snippet above.
104+
105+
```json
106+
{
107+
"username": [
108+
"The username field must only contain single-byte alphanumeric characters and symbols."
109+
],
110+
"name": [
111+
"The name field is required."
112+
],
113+
"email": [
114+
"The email field must be a valid email address."
115+
],
116+
"phone_number": [
117+
"The phone number field must be a valid phone number."
118+
],
119+
"letters.0": [
120+
"The letters.0 field has a duplicate value."
121+
],
122+
"letters.1": [
123+
"The letters.1 field has a duplicate value."
124+
],
125+
"letters.2": [
126+
"The letters.2 field has a duplicate value."
127+
],
128+
"letters.3": [
129+
"The letters.3 field has a duplicate value."
130+
],
131+
"items": [
132+
"The size of the array must be equal to 5 items only."
133+
],
134+
"items.y": [
135+
"The items.y field is required."
136+
],
137+
"items.z": [
138+
"The items.z field must be a string."
139+
],
140+
"cc_number": [
141+
"The Credit Card Number field is required when payment type is credit card."
142+
]
143+
}
144+
```
145+
146+
## Security Vulnerabilities
147+
148+
If you discover any security related issues, please email <hafizuddin_83@yahoo.com> instead of using the issue tracker. Please prefix the subject with `Quival:`.
149+
150+
## Credits
151+
152+
- [Mohd Hafizuddin M Marzuki](https://github.com/apih)
153+
- [All Contributors](../../contributors)
154+
155+
## License
156+
157+
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

‎package-lock.json

+1,141
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "quival",
3+
"version": "0.1.0",
4+
"description": "Data validation à la Laravel Validation",
5+
"author": "Mohd Hafizuddin M Marzuki <hafizuddin_83@yahoo.com>",
6+
"license": "MIT",
7+
"type": "module",
8+
"main": "./src/index.js",
9+
"keywords": [
10+
"data validation",
11+
"form",
12+
"front end",
13+
"laravel",
14+
"quick",
15+
"validation",
16+
"validator"
17+
],
18+
"repository": {
19+
"type": "git",
20+
"url": "https://github.com/apih/quival"
21+
},
22+
"devDependencies": {
23+
"eslint": "^8.39.0",
24+
"prettier": "^2.8.8"
25+
},
26+
"scripts": {}
27+
}

‎src/Checkers.js

+876
Large diffs are not rendered by default.

‎src/ErrorBag.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export default class ErrorBag {
2+
#data = {};
3+
4+
add(key, message) {
5+
if (this.#data.hasOwnProperty(key)) {
6+
this.#data[key].push(message);
7+
} else {
8+
this.#data[key] = [message];
9+
}
10+
}
11+
12+
get(key) {
13+
if (!key.includes('*')) {
14+
return this.#data.hasOwnProperty(key) ? this.#data[key] : {};
15+
}
16+
17+
const pattern = new RegExp('^' + key.replaceAll('*', '.*?') + '$');
18+
const result = {};
19+
20+
for (const [key, value] of Object.entries(this.#data)) {
21+
if (pattern.test(key)) {
22+
result[key] = value;
23+
}
24+
}
25+
26+
return result;
27+
}
28+
29+
first(key) {
30+
for (const message of Object.values(this.get(key))) {
31+
return Array.isArray(message) ? message[0] : message;
32+
}
33+
34+
return '';
35+
}
36+
37+
has(key) {
38+
return this.first(key) !== '';
39+
}
40+
41+
messages() {
42+
return this.#data;
43+
}
44+
45+
all() {
46+
const result = [];
47+
48+
Object.values(this.#data).forEach((messages) => result.push(...messages));
49+
50+
return result;
51+
}
52+
53+
count() {
54+
let count = 0;
55+
56+
Object.values(this.#data).forEach((messages) => (count += messages.length));
57+
58+
return count;
59+
}
60+
61+
isEmpty() {
62+
return Object.keys(this.#data).length === 0;
63+
}
64+
65+
isNotEmpty() {
66+
return !this.isEmpty();
67+
}
68+
}

‎src/Lang.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { flattenObject, isPlainObject } from './helpers.js';
2+
3+
export default class Lang {
4+
static #locale;
5+
static #messages = {};
6+
7+
static locale(locale) {
8+
this.#locale = locale;
9+
}
10+
11+
static setMessages(locale, messages) {
12+
this.#messages[locale] = flattenObject(messages);
13+
}
14+
15+
static get(path) {
16+
return this.#messages[this.#locale][path];
17+
}
18+
19+
static has(path) {
20+
return typeof this.get(path) === 'undefined' ? false : true;
21+
}
22+
23+
static set(path, value) {
24+
if (isPlainObject(value)) {
25+
Object.assign(this.#messages[this.#locale], flattenObject(value, path));
26+
} else if (typeof value === 'string') {
27+
this.#messages[this.#locale][path] = value;
28+
}
29+
}
30+
}

‎src/Replacers.js

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
export default class Replacers {
2+
validator;
3+
4+
constructor(validator) {
5+
this.validator = validator;
6+
}
7+
8+
replace(message, data) {
9+
Object.entries(data).forEach(([key, value]) => (message = message.replaceAll(':' + key, value)));
10+
11+
return message;
12+
}
13+
14+
// Numeric
15+
replaceDecimal(message, attribute, rule, parameters) {
16+
return this.replace(message, {
17+
decimal: parameters.join('-'),
18+
});
19+
}
20+
21+
replaceMultipleOf(message, attribute, rule, parameters) {
22+
return this.replace(message, {
23+
value: parameters[0],
24+
});
25+
}
26+
27+
// Agreement
28+
replaceAcceptedIf(message, attribute, rule, parameters) {
29+
return this.replace(message, {
30+
other: this.validator.getDisplayableAttribute(parameters[0]),
31+
value: this.validator.getDisplayableValue(parameters[0], this.validator.getValue(parameters[0])),
32+
});
33+
}
34+
35+
replaceDeclinedIf(message, attribute, rule, parameters) {
36+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
37+
}
38+
39+
// Existence
40+
replaceRequiredArrayKeys(message, attribute, rule, parameters) {
41+
return this.replace(message, {
42+
values: parameters.map((value) => this.validator.getDisplayableValue(attribute, value)).join(', '),
43+
});
44+
}
45+
46+
replaceRequiredIf(message, attribute, rule, parameters) {
47+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
48+
}
49+
50+
replaceRequiredIfAccepted(message, attribute, rule, parameters) {
51+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
52+
}
53+
54+
replaceRequiredUnless(message, attribute, rule, parameters) {
55+
return this.replace(message, {
56+
other: this.validator.getDisplayableAttribute(parameters[0]),
57+
values: parameters
58+
.slice(1)
59+
.map((value) => this.validator.getDisplayableValue(parameters[0], value))
60+
.join(', '),
61+
});
62+
}
63+
64+
replaceRequiredWith(message, attribute, rule, parameters) {
65+
return this.replace(message, {
66+
values: parameters.map((value) => this.validator.getDisplayableAttribute(value)).join(' / '),
67+
});
68+
}
69+
70+
replaceRequiredWithAll(message, attribute, rule, parameters) {
71+
return this.replaceRequiredWith(message, attribute, rule, parameters);
72+
}
73+
74+
replaceRequiredWithout(message, attribute, rule, parameters) {
75+
return this.replaceRequiredWith(message, attribute, rule, parameters);
76+
}
77+
78+
replaceRequiredWithoutAll(message, attribute, rule, parameters) {
79+
return this.replaceRequiredWith(message, attribute, rule, parameters);
80+
}
81+
82+
// Missing
83+
replaceMissingIf(message, attribute, rule, parameters) {
84+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
85+
}
86+
87+
replaceMissingUnless(message, attribute, rule, parameters) {
88+
return this.replace(this.replaceRequiredUnless(message, attribute, rule, parameters), {
89+
value: this.validator.getDisplayableValue(parameters[0], parameters[1]),
90+
});
91+
}
92+
93+
replaceMissingWith(message, attribute, rule, parameters) {
94+
return this.replaceRequiredWith(message, attribute, rule, parameters);
95+
}
96+
97+
replaceMissingWithAll(message, attribute, rule, parameters) {
98+
return this.replaceRequiredWith(message, attribute, rule, parameters);
99+
}
100+
101+
// Prohibition
102+
replaceProhibitedIf(message, attribute, rule, parameters) {
103+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
104+
}
105+
106+
replaceProhibitedUnless(message, attribute, rule, parameters) {
107+
return this.replaceRequiredUnless(message, attribute, rule, parameters);
108+
}
109+
110+
replaceProhibits(message, attribute, rule, parameters) {
111+
return this.replace(message, {
112+
other: parameters.map((value) => this.validator.getDisplayableAttribute(value)).join(' / '),
113+
});
114+
}
115+
116+
// Size
117+
replaceSize(message, attribute, rule, parameters) {
118+
return this.replace(message, {
119+
size: parameters[0],
120+
});
121+
}
122+
123+
replaceMin(message, attribute, rule, parameters) {
124+
return this.replace(message, {
125+
min: parameters[0],
126+
});
127+
}
128+
129+
replaceMax(message, attribute, rule, parameters) {
130+
return this.replace(message, {
131+
max: parameters[0],
132+
});
133+
}
134+
135+
replaceBetween(message, attribute, rule, parameters) {
136+
return this.replace(message, {
137+
min: parameters[0],
138+
max: parameters[1],
139+
});
140+
}
141+
142+
// Digits
143+
replaceDigits(message, attribute, rule, parameters) {
144+
return this.replace(message, {
145+
digits: parameters[0],
146+
});
147+
}
148+
149+
replaceMinDigits(message, attribute, rule, parameters) {
150+
return this.replaceMin(message, attribute, rule, parameters);
151+
}
152+
153+
replaceMaxDigits(message, attribute, rule, parameters) {
154+
return this.replaceMax(message, attribute, rule, parameters);
155+
}
156+
157+
replaceDigitsBetween(message, attribute, rule, parameters) {
158+
return this.replaceBetween(message, attribute, rule, parameters);
159+
}
160+
161+
// String
162+
replaceStartsWith(message, attribute, rule, parameters) {
163+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
164+
}
165+
166+
replaceDoesntStartWith(message, attribute, rule, parameters) {
167+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
168+
}
169+
170+
replaceEndsWith(message, attribute, rule, parameters) {
171+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
172+
}
173+
174+
replaceDoesntEndWith(message, attribute, rule, parameters) {
175+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
176+
}
177+
178+
// Compare values
179+
replaceSame(message, attribute, rule, parameters) {
180+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
181+
}
182+
183+
replaceDifferent(message, attribute, rule, parameters) {
184+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
185+
}
186+
187+
replaceGt(message, attribute, rule, parameters) {
188+
const value = this.validator.getValue(parameters[0]);
189+
190+
return this.replace(message, {
191+
value: value ? this.validator.getSize(parameters[0], value) : this.validator.getDisplayableAttribute(parameters[0]),
192+
});
193+
}
194+
195+
replaceGte(message, attribute, rule, parameters) {
196+
return this.replaceGt(message, attribute, rule, parameters);
197+
}
198+
199+
replaceLt(message, attribute, rule, parameters) {
200+
return this.replaceGt(message, attribute, rule, parameters);
201+
}
202+
replaceLte(message, attribute, rule, parameters) {
203+
return this.replaceGt(message, attribute, rule, parameters);
204+
}
205+
206+
// Dates
207+
replaceAfter(message, attribute, rule, parameters) {
208+
const other = parameters[0];
209+
210+
return this.replace(message, {
211+
date: this.validator.hasAttribute(other) ? this.validator.getDisplayableAttribute(other) : other,
212+
});
213+
}
214+
215+
replaceAfterOrEqual(message, attribute, rule, parameters) {
216+
return this.replaceAfter(message, attribute, rule, parameters);
217+
}
218+
219+
replaceBefore(message, attribute, rule, parameters) {
220+
return this.replaceAfter(message, attribute, rule, parameters);
221+
}
222+
223+
replaceBeforeOrEqual(message, attribute, rule, parameters) {
224+
return this.replaceAfter(message, attribute, rule, parameters);
225+
}
226+
227+
replaceDateEquals(message, attribute, rule, parameters) {
228+
return this.replaceAfter(message, attribute, rule, parameters);
229+
}
230+
231+
replaceDateFormat(message, attribute, rule, parameters) {
232+
return this.replace(message, {
233+
format: parameters[0],
234+
});
235+
}
236+
237+
// Array
238+
replaceInArray(message, attribute, rule, parameters) {
239+
return this.replaceAcceptedIf(message, attribute, rule, parameters);
240+
}
241+
242+
replaceIn(message, attribute, rule, parameters) {
243+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
244+
}
245+
246+
replaceNotIn(message, attribute, rule, parameters) {
247+
return this.replaceRequiredArrayKeys(message, attribute, rule, parameters);
248+
}
249+
250+
// File
251+
replaceMimetypes(message, attribute, rule, parameters) {
252+
return this.replace(message, {
253+
values: parameters.join(', '),
254+
});
255+
}
256+
257+
replaceMimes(message, attribute, rule, parameters) {
258+
return this.replaceMimetypes(message, attribute, rule, parameters);
259+
}
260+
}

‎src/Validator.js

+446
Large diffs are not rendered by default.

‎src/helpers.js

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
export function toCamelCase(string) {
2+
return string.replace(/(_\w)/g, (match) => match[1].toUpperCase());
3+
}
4+
5+
export function toSnakeCase(string) {
6+
return string.replace(/[\w]([A-Z])/g, (match) => match[0] + '_' + match[1].toLowerCase()).toLowerCase();
7+
}
8+
9+
export function escapeRegExp(string) {
10+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11+
}
12+
13+
export function getByPath(obj, path, defaultValue) {
14+
const keys = path.split('.');
15+
let current = obj;
16+
17+
for (const key of keys) {
18+
if (!current.hasOwnProperty(key)) {
19+
return defaultValue;
20+
}
21+
22+
current = current[key];
23+
}
24+
25+
return current;
26+
}
27+
28+
export function setByPath(obj, path, value) {
29+
const keys = path.split('.');
30+
let current = obj;
31+
32+
for (const key of keys.slice(0, -1)) {
33+
if (!current.hasOwnProperty(key)) {
34+
current[key] = {};
35+
}
36+
37+
current = current[key];
38+
}
39+
40+
current[keys.slice(-1)] = value;
41+
}
42+
43+
export function flattenObject(obj, prefix = '') {
44+
return Object.keys(obj).reduce((accumulator, key) => {
45+
const prefixedKey = prefix ? `${prefix}.${key}` : key;
46+
47+
if (typeof obj[key] === 'object' && obj[key] !== null) {
48+
Object.assign(accumulator, flattenObject(obj[key], prefixedKey));
49+
} else {
50+
accumulator[prefixedKey] = obj[key];
51+
}
52+
53+
return accumulator;
54+
}, {});
55+
}
56+
57+
export function parseDate(value) {
58+
if (isEmpty(value)) {
59+
return new Date('');
60+
} else if (value instanceof Date) {
61+
return value;
62+
}
63+
64+
let match, years, months, days, hours, minutes, seconds, meridiem;
65+
66+
const castToIntegers = (value) => (value && /^\d*$/.test(value) ? parseInt(value) : value);
67+
68+
if ((match = value.match(/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\s?((\d{1,2}):(\d{1,2})(:(\d{1,2}))?\s?(am|pm)?)?/i)) !== null) {
69+
[, days, months, years, , hours = 0, minutes = 0, , seconds = 0, meridiem = 'am'] = match.map(castToIntegers);
70+
} else if (
71+
(match = value.match(/^(\d{2,4})[.\/-](\d{1,2})[.\/-](\d{1,2})\s?((\d{1,2}):(\d{1,2})(:(\d{1,2}))?\s?(am|pm)?)?/i)) !== null ||
72+
(match = value.match(/^(\d{4})(\d{2})(\d{2})\s?((\d{2})(\d{2})((\d{2}))?\s?(am|pm)?)?/i)) !== null
73+
) {
74+
[, years, months, days, , hours = 0, minutes = 0, , seconds = 0, meridiem = 'am'] = match.map(castToIntegers);
75+
} else if ((match = value.match(/(\d{1,2}):(\d{1,2})(:(\d{1,2}))?\s?(am|pm)?\s?(\d{4})[.\/-](\d{2})[.\/-](\d{2})/i))) {
76+
[, hours, minutes, , seconds, meridiem = 'am', years, months, days] = match.map(castToIntegers);
77+
} else if ((match = value.match(/(\d{1,2}):(\d{1,2})(:(\d{1,2}))?\s?(am|pm)?\s?(\d{2})[.\/-](\d{2})[.\/-](\d{4})/i))) {
78+
[, hours, minutes, , seconds, meridiem = 'am', days, months, years] = match.map(castToIntegers);
79+
} else if ((match = value.match(/(\d{1,2}):(\d{1,2})(:(\d{1,2}))?\s?(am|pm)?/i))) {
80+
const current = new Date();
81+
82+
years = current.getFullYear();
83+
months = current.getMonth() + 1;
84+
days = current.getDate();
85+
86+
[, hours = 0, minutes = 0, , seconds = 0, meridiem = 'am'] = match.map(castToIntegers);
87+
} else {
88+
return new Date(value);
89+
}
90+
91+
if (years >= 10 && years < 100) {
92+
years += 2000;
93+
}
94+
95+
if (meridiem.toLowerCase() === 'pm' && hours < 12) {
96+
hours += 12;
97+
}
98+
99+
return new Date(`${years}-${months}-${days} ${hours}:${minutes}:${seconds}`);
100+
}
101+
102+
export function parseDateByFormat(value, format) {
103+
if (isEmpty(value)) {
104+
return new Date('');
105+
}
106+
107+
format = format.split('');
108+
109+
const formats = {
110+
Y: '(\\d{4})',
111+
y: '(\\d{2})',
112+
m: '(\\d{2})',
113+
n: '([1-9]\\d?)',
114+
d: '(\\d{2})',
115+
j: '([1-9]\\d?)',
116+
G: '([1-9]\\d?)',
117+
g: '([1-9]\\d?)',
118+
H: '(\\d{2})',
119+
h: '(\\d{2})',
120+
i: '(\\d{2})',
121+
s: '(\\d{2})',
122+
A: '(AM|PM)',
123+
a: '(am|pm)',
124+
};
125+
126+
let pattern = '^';
127+
let indices = {
128+
years: -1,
129+
months: -1,
130+
days: -1,
131+
hours: -1,
132+
minutes: -1,
133+
seconds: -1,
134+
meridiem: -1,
135+
};
136+
137+
let index = 1;
138+
139+
for (const char of format) {
140+
if (formats.hasOwnProperty(char)) {
141+
pattern += formats[char];
142+
143+
if (['Y', 'y'].indexOf(char) !== -1) {
144+
indices.years = index++;
145+
} else if (['m', 'n'].indexOf(char) !== -1) {
146+
indices.months = index++;
147+
} else if (['d', 'j'].indexOf(char) !== -1) {
148+
indices.days = index++;
149+
} else if (['G', 'g', 'H', 'h'].indexOf(char) !== -1) {
150+
indices.hours = index++;
151+
} else if (char === 'i') {
152+
indices.minutes = index++;
153+
} else if (char === 's') {
154+
indices.seconds = index++;
155+
} else if (['A', 'a'].indexOf(char) !== -1) {
156+
indices.meridiem = index++;
157+
}
158+
} else {
159+
pattern += '\\' + char;
160+
}
161+
}
162+
163+
pattern += '$';
164+
165+
let match = value.match(new RegExp(pattern));
166+
167+
if (match === null) {
168+
return new Date('');
169+
}
170+
171+
match = match.map((value) => (value && /^\d*$/.test(value) ? parseInt(value) : value));
172+
173+
const current = new Date();
174+
175+
let years = match[indices.years];
176+
let months = match[indices.months];
177+
let days = match[indices.days];
178+
let hours = match[indices.hours] ?? 0;
179+
let minutes = match[indices.minutes] ?? 0;
180+
let seconds = match[indices.seconds] ?? 0;
181+
let meridiem = match[indices.meridiem] ?? 'am';
182+
183+
if (!years && !months && !days) {
184+
years = current.getFullYear();
185+
months = current.getMonth() + 1;
186+
days = current.getDate();
187+
} else if (years && !months && !days) {
188+
months = 1;
189+
days = 1;
190+
} else if (!years && months && !days) {
191+
years = current.getFullYear();
192+
days = 1;
193+
} else if (!years && !months && days) {
194+
years = current.getFullYear();
195+
months = current.getMonth() + 1;
196+
}
197+
198+
if (years >= 10 && years < 100) {
199+
years = years + 2000;
200+
}
201+
202+
if (meridiem.toLowerCase() === 'pm' && hours < 12) {
203+
hours += 12;
204+
}
205+
206+
return new Date(`${years}-${months}-${days} ${hours}:${minutes}:${seconds}`);
207+
}
208+
209+
export function isDigits(value) {
210+
return String(value).search(/[^0-9]/) === -1;
211+
}
212+
213+
export function isEmpty(value) {
214+
return value === '' || value === null || typeof value === 'undefined';
215+
}
216+
217+
export function isNumeric(value) {
218+
const number = Number(value);
219+
220+
return value !== null && typeof value !== 'boolean' && typeof number === 'number' && !isNaN(number);
221+
}
222+
223+
export function isPlainObject(value) {
224+
return Object.prototype.toString.call(value) === '[object Object]';
225+
}
226+
227+
export function isValidDate(value) {
228+
return value instanceof Date && value.toDateString() !== 'Invalid Date';
229+
}

‎src/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Checkers from './Checkers.js';
2+
import ErrorBag from './ErrorBag.js';
3+
import Lang from './Lang.js';
4+
import Replacers from './Replacers.js';
5+
import Validator from './Validator.js';
6+
7+
export { Checkers, ErrorBag, Lang, Replacers, Validator };

‎src/locales/en.js

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
export default {
2+
/**
3+
* Default error messages
4+
*/
5+
accepted: 'The :attribute field must be accepted.',
6+
accepted_if: 'The :attribute field must be accepted when :other is :value.',
7+
active_url: 'The :attribute field must be a valid URL.',
8+
after: 'The :attribute field must be a date after :date.',
9+
after_or_equal: 'The :attribute field must be a date after or equal to :date.',
10+
alpha: 'The :attribute field must only contain letters.',
11+
alpha_dash: 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
12+
alpha_num: 'The :attribute field must only contain letters and numbers.',
13+
array: 'The :attribute field must be an array.',
14+
ascii: 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
15+
before: 'The :attribute field must be a date before :date.',
16+
before_or_equal: 'The :attribute field must be a date before or equal to :date.',
17+
between: {
18+
array: 'The :attribute field must have between :min and :max items.',
19+
file: 'The :attribute field must be between :min and :max kilobytes.',
20+
numeric: 'The :attribute field must be between :min and :max.',
21+
string: 'The :attribute field must be between :min and :max characters.',
22+
},
23+
boolean: 'The :attribute field must be true or false.',
24+
confirmed: 'The :attribute field confirmation does not match.',
25+
current_password: 'The password is incorrect.',
26+
date: 'The :attribute field must be a valid date.',
27+
date_equals: 'The :attribute field must be a date equal to :date.',
28+
date_format: 'The :attribute field must match the format :format.',
29+
decimal: 'The :attribute field must have :decimal decimal places.',
30+
declined: 'The :attribute field must be declined.',
31+
declined_if: 'The :attribute field must be declined when :other is :value.',
32+
different: 'The :attribute field and :other must be different.',
33+
digits: 'The :attribute field must be :digits digits.',
34+
digits_between: 'The :attribute field must be between :min and :max digits.',
35+
dimensions: 'The :attribute field has invalid image dimensions.',
36+
distinct: 'The :attribute field has a duplicate value.',
37+
doesnt_end_with: 'The :attribute field must not end with one of the following: :values.',
38+
doesnt_start_with: 'The :attribute field must not start with one of the following: :values.',
39+
email: 'The :attribute field must be a valid email address.',
40+
ends_with: 'The :attribute field must end with one of the following: :values.',
41+
enum: 'The selected :attribute is invalid.',
42+
exists: 'The selected :attribute is invalid.',
43+
file: 'The :attribute field must be a file.',
44+
filled: 'The :attribute field must have a value.',
45+
gt: {
46+
array: 'The :attribute field must have more than :value items.',
47+
file: 'The :attribute field must be greater than :value kilobytes.',
48+
numeric: 'The :attribute field must be greater than :value.',
49+
string: 'The :attribute field must be greater than :value characters.',
50+
},
51+
gte: {
52+
array: 'The :attribute field must have :value items or more.',
53+
file: 'The :attribute field must be greater than or equal to :value kilobytes.',
54+
numeric: 'The :attribute field must be greater than or equal to :value.',
55+
string: 'The :attribute field must be greater than or equal to :value characters.',
56+
},
57+
image: 'The :attribute field must be an image.',
58+
in: 'The selected :attribute is invalid.',
59+
in_array: 'The :attribute field must exist in :other.',
60+
integer: 'The :attribute field must be an integer.',
61+
ip: 'The :attribute field must be a valid IP address.',
62+
ipv4: 'The :attribute field must be a valid IPv4 address.',
63+
ipv6: 'The :attribute field must be a valid IPv6 address.',
64+
json: 'The :attribute field must be a valid JSON string.',
65+
lowercase: 'The :attribute field must be lowercase.',
66+
lt: {
67+
array: 'The :attribute field must have less than :value items.',
68+
file: 'The :attribute field must be less than :value kilobytes.',
69+
numeric: 'The :attribute field must be less than :value.',
70+
string: 'The :attribute field must be less than :value characters.',
71+
},
72+
lte: {
73+
array: 'The :attribute field must not have more than :value items.',
74+
file: 'The :attribute field must be less than or equal to :value kilobytes.',
75+
numeric: 'The :attribute field must be less than or equal to :value.',
76+
string: 'The :attribute field must be less than or equal to :value characters.',
77+
},
78+
mac_address: 'The :attribute field must be a valid MAC address.',
79+
max: {
80+
array: 'The :attribute field must not have more than :max items.',
81+
file: 'The :attribute field must not be greater than :max kilobytes.',
82+
numeric: 'The :attribute field must not be greater than :max.',
83+
string: 'The :attribute field must not be greater than :max characters.',
84+
},
85+
max_digits: 'The :attribute field must not have more than :max digits.',
86+
mimes: 'The :attribute field must be a file of type: :values.',
87+
mimetypes: 'The :attribute field must be a file of type: :values.',
88+
min: {
89+
array: 'The :attribute field must have at least :min items.',
90+
file: 'The :attribute field must be at least :min kilobytes.',
91+
numeric: 'The :attribute field must be at least :min.',
92+
string: 'The :attribute field must be at least :min characters.',
93+
},
94+
min_digits: 'The :attribute field must have at least :min digits.',
95+
missing: 'The :attribute field must be missing.',
96+
missing_if: 'The :attribute field must be missing when :other is :value.',
97+
missing_unless: 'The :attribute field must be missing unless :other is :value.',
98+
missing_with: 'The :attribute field must be missing when :values is present.',
99+
missing_with_all: 'The :attribute field must be missing when :values are present.',
100+
multiple_of: 'The :attribute field must be a multiple of :value.',
101+
not_in: 'The selected :attribute is invalid.',
102+
not_regex: 'The :attribute field format is invalid.',
103+
numeric: 'The :attribute field must be a number.',
104+
password: {
105+
letters: 'The :attribute field must contain at least one letter.',
106+
mixed: 'The :attribute field must contain at least one uppercase and one lowercase letter.',
107+
numbers: 'The :attribute field must contain at least one number.',
108+
symbols: 'The :attribute field must contain at least one symbol.',
109+
uncompromised: 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
110+
},
111+
present: 'The :attribute field must be present.',
112+
prohibited: 'The :attribute field is prohibited.',
113+
prohibited_if: 'The :attribute field is prohibited when :other is :value.',
114+
prohibited_unless: 'The :attribute field is prohibited unless :other is in :values.',
115+
prohibits: 'The :attribute field prohibits :other from being present.',
116+
regex: 'The :attribute field format is invalid.',
117+
required: 'The :attribute field is required.',
118+
required_array_keys: 'The :attribute field must contain entries for: :values.',
119+
required_if: 'The :attribute field is required when :other is :value.',
120+
required_if_accepted: 'The :attribute field is required when :other is accepted.',
121+
required_unless: 'The :attribute field is required unless :other is in :values.',
122+
required_with: 'The :attribute field is required when :values is present.',
123+
required_with_all: 'The :attribute field is required when :values are present.',
124+
required_without: 'The :attribute field is required when :values is not present.',
125+
required_without_all: 'The :attribute field is required when none of :values are present.',
126+
same: 'The :attribute field must match :other.',
127+
size: {
128+
array: 'The :attribute field must contain :size items.',
129+
file: 'The :attribute field must be :size kilobytes.',
130+
numeric: 'The :attribute field must be :size.',
131+
string: 'The :attribute field must be :size characters.',
132+
},
133+
starts_with: 'The :attribute field must start with one of the following: :values.',
134+
string: 'The :attribute field must be a string.',
135+
timezone: 'The :attribute field must be a valid timezone.',
136+
unique: 'The :attribute has already been taken.',
137+
uploaded: 'The :attribute failed to upload.',
138+
uppercase: 'The :attribute field must be uppercase.',
139+
url: 'The :attribute field must be a valid URL.',
140+
ulid: 'The :attribute field must be a valid ULID.',
141+
uuid: 'The :attribute field must be a valid UUID.',
142+
143+
/**
144+
* Custom error messages
145+
*/
146+
custom: {
147+
'attribute-name': {
148+
'rule-name': 'custom-message',
149+
},
150+
},
151+
152+
/**
153+
* Custom attributes
154+
*/
155+
attributes: {},
156+
157+
/**
158+
* Custom values
159+
*/
160+
values: {},
161+
};

‎src/locales/ms.js

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
export default {
2+
/**
3+
* Default error messages
4+
*/
5+
accepted: 'Medan :attribute mesti diterima.',
6+
accepted_if: 'Medan :attribute mesti diterima apabila :other ialah :value.',
7+
active_url: 'Medan :attribute mesti URL yang sah.',
8+
after: 'Medan :attribute mesti tarikh selepas :date.',
9+
after_or_equal: 'Medan :attribute mesti tarikh selepas atau sama dengan :date.',
10+
alpha: 'Medan :attribute hanya boleh mengandungi huruf.',
11+
alpha_dash: 'Medan :attribute hanya boleh mengandungi huruf, nombor, tanda sengkang, dan garis bawah.',
12+
alpha_num: 'Medan :attribute hanya boleh mengandungi huruf dan nombor.',
13+
array: 'Medan :attribute mesti jujukan.',
14+
ascii: 'Medan :attribute hanya boleh mengandungi abjad, angka dan simbol berjenis bait tunggal.',
15+
before: 'Medan :attribute mesti tarikh sebelum :date.',
16+
before_or_equal: 'Medan :attribute mesti tarikh sebelum atau sama dengan :date.',
17+
between: {
18+
array: 'Medan :attribute mesti mempunyai antara :min dan :max item.',
19+
file: 'Medan :attribute mesti antara :min dan :max kilobait.',
20+
numeric: 'Medan :attribute mesti antara :min dan :max.',
21+
string: 'Medan :attribute mesti antara :min dan :max huruf.',
22+
},
23+
boolean: 'Medan :attribute mesti benar atau salah.',
24+
confirmed: 'Pengesahan medan :attribute tidak sepadan.',
25+
current_password: 'Kata laluan tidak sah.',
26+
date: 'Medan :attribute mesti tarikh yang sah.',
27+
date_equals: 'Medan :attribute mesti bersamaan dengan :date.',
28+
date_format: 'Medan :attribute mesti sepadan dengan format :format.',
29+
decimal: 'Medan :attribute mesti mempunyai :decimal tempat perpuluhan.',
30+
declined: 'Medan :attribute mesti ditolak.',
31+
declined_if: 'Medan :attribute mesti ditolak apabila :other adalah :value.',
32+
different: 'Medan :attribute dan :other mesti berbeza.',
33+
digits: 'Medan :attribute mesti mengandungi :digits digit.',
34+
digits_between: 'Medan :attribute mesti mengandungi antara :min dan :max digit.',
35+
dimensions: 'Medan :attribute mempunyai dimensi imej yang tidak sah.',
36+
distinct: 'Medan :attribute mempunyai nilai yang berulang.',
37+
doesnt_end_with: 'Medan :attribute tidak boleh berakhir dengan salah satu daripada berikut: :values.',
38+
doesnt_start_with: 'Medan :attribute tidak boleh bermula dengan salah satu daripada berikut: :values.',
39+
email: 'Medan :attribute mesti alamat emel yang sah.',
40+
ends_with: 'Medan :attribute mesti berakhir dengan salah satu daripada berikut: :values.',
41+
enum: 'Nilai :attribute yang dipilih tidak sah.',
42+
exists: 'Nilai :attribute yang dipilih tidak sah.',
43+
file: 'Medan :attribute mesti fail.',
44+
filled: 'Medan :attribute mesti mempunyai nilai.',
45+
gt: {
46+
array: 'Medan :attribute mesti mempunyai lebih daripada :value item.',
47+
file: 'Medan :attribute mesti lebih besar daripada :value kilobait.',
48+
numeric: 'Medan :attribute mesti lebih besar daripada :value.',
49+
string: 'Medan :attribute mesti lebih besar daripada :value huruf.',
50+
},
51+
gte: {
52+
array: 'Medan :attribute mesti mempunyai :value item atau lebih.',
53+
file: 'Medan :attribute mesti lebih besar daripada atau sama dengan :value kilobait.',
54+
numeric: 'Medan :attribute mesti lebih besar daripada atau sama dengan :value.',
55+
string: 'Medan :attribute mesti lebih besar daripada atau sama dengan :value huruf.',
56+
},
57+
image: 'Medan :attribute mesti imej.',
58+
in: 'Nilai :attribute yang dipilih tidak sah.',
59+
in_array: 'Medan :attribute mesti wujud dalam :other.',
60+
integer: 'Medan :attribute mesti nombor bulat.',
61+
ip: 'Medan :attribute mesti alamat IP yang sah.',
62+
ipv4: 'Medan :attribute mesti alamat IPv4 yang sah.',
63+
ipv6: 'Medan :attribute mesti alamat IPv6 yang sah.',
64+
json: 'Medan :attribute mesti rentetan JSON yang sah.',
65+
lowercase: 'Medan :attribute mesti dalam huruf kecil.',
66+
lt: {
67+
array: 'Medan :attribute mesti mempunyai kurang daripada :value item.',
68+
file: 'Medan :attribute mesti kurang daripada :value kilobait.',
69+
numeric: 'Medan :attribute mesti kurang daripada :value.',
70+
string: 'Medan :attribute mesti kurang daripada :value huruf.',
71+
},
72+
lte: {
73+
array: 'Medan :attribute tidak boleh mempunyai lebih daripada :value item.',
74+
file: 'Medan :attribute mesti kurang daripada atau sama dengan :value kilobait.',
75+
numeric: 'Medan :attribute mesti kurang daripada atau sama dengan :value.',
76+
string: 'Medan :attribute mesti kurang daripada atau sama dengan :value huruf.',
77+
},
78+
mac_address: 'Medan :attribute mesti alamat MAC yang sah.',
79+
max: {
80+
array: 'Medan :attribute tidak boleh mempunyai lebih daripada :max item.',
81+
file: 'Medan :attribute tidak boleh lebih besar daripada :max kilobait.',
82+
numeric: 'Medan :attribute tidak boleh melebihi :max.',
83+
string: 'Medan :attribute tidak boleh melebihi :max huruf.',
84+
},
85+
max_digits: 'Medan :attribute tidak boleh mempunyai lebih daripada :max digit.',
86+
mimes: 'Medan :attribute mesti jenis fail: :values.',
87+
mimetypes: 'Medan :attribute mesti jenis fail: :values.',
88+
min: {
89+
array: 'Medan :attribute mesti mempunyai sekurang-kurangnya :min item.',
90+
file: 'Medan :attribute mesti sekurang-kurangnya :min kilobait.',
91+
numeric: 'Medan :attribute mesti sekurang-kurangnya :min.',
92+
string: 'Medan :attribute mesti sekurang-kurangnya :min huruf.',
93+
},
94+
min_digits: 'Medan :attribute mesti mempunyai sekurang-kurangnya :min digit.',
95+
missing: 'Medan :attribute mesti tiada.',
96+
missing_if: 'Medan :attribute mesti tiada apabila :other adalah :value.',
97+
missing_unless: 'Medan :attribute mesti tiada kecuali :other adalah :value.',
98+
missing_with: 'Medan :attribute mesti tiada apabila :values wujud.',
99+
missing_with_all: 'Medan :attribute mesti tiada apabila :values wujud.',
100+
multiple_of: 'Medan :attribute mesti gandaan :value.',
101+
not_in: 'Nilai :attribute yang dipilih tidak sah.',
102+
not_regex: 'Format medan :attribute tidak sah.',
103+
numeric: 'Medan :attribute mesti nombor.',
104+
password: {
105+
letters: 'Medan :attribute mesti mengandungi sekurang-kurangnya satu huruf.',
106+
mixed: 'Medan :attribute mesti mengandungi sekurang-kurangnya satu huruf besar dan satu huruf kecil.',
107+
numbers: 'Medan :attribute mesti mengandungi sekurang-kurangnya satu nombor.',
108+
symbols: 'Medan :attribute mesti mengandungi sekurang-kurangnya satu simbol.',
109+
uncompromised: 'Nilai :attribute yang diberikan telah muncul dalam kebocoran data. Sila pilih :attribute yang berbeza.',
110+
},
111+
present: 'Medan :attribute mesti wujud.',
112+
prohibited: 'Medan :attribute dilarang.',
113+
prohibited_if: 'Medan :attribute dilarang apabila :other adalah :value.',
114+
prohibited_unless: 'Medan :attribute dilarang melainkan :other berada dalam :values.',
115+
prohibits: 'Medan :attribute melarang :other daripada wujud.',
116+
regex: 'Format medan :attribute tidak sah.',
117+
required: 'Medan :attribute diperlukan.',
118+
required_array_keys: 'Medan :attribute mesti mengandungi entri untuk: :values.',
119+
required_if: 'Medan :attribute diperlukan apabila :other adalah :value.',
120+
required_if_accepted: 'Medan :attribute diperlukan apabila :other diterima.',
121+
required_unless: 'Medan :attribute diperlukan melainkan :other berada dalam :values.',
122+
required_with: 'Medan :attribute diperlukan apabila :values wujud.',
123+
required_with_all: 'Medan :attribute diperlukan apabila semua :values wujud.',
124+
required_without: 'Medan :attribute diperlukan apabila :values tidak wujud.',
125+
required_without_all: 'Medan :attribute diperlukan apabila tiada satu pun daripada :values wujud.',
126+
same: 'Medan :attribute mesti sepadan dengan :other.',
127+
size: {
128+
array: 'Medan :attribute mesti mengandungi :size item.',
129+
file: 'Medan :attribute mesti :size kilobait.',
130+
numeric: 'Medan :attribute mesti :size.',
131+
string: 'Medan :attribute mesti :size huruf.',
132+
},
133+
starts_with: 'Medan :attribute mesti bermula dengan salah satu dari berikut: :values.',
134+
string: 'Medan :attribute mesti perkataan / rentetan aksara.',
135+
timezone: 'Medan :attribute mesti zon masa yang sah.',
136+
unique: 'Medan :attribute telah wujud.',
137+
uploaded: 'Medan :attribute gagal dimuat naik.',
138+
uppercase: 'Medan :attribute mesti dalam huruf besar.',
139+
url: 'Medan :attribute mesti URL yang sah.',
140+
ulid: 'Medan :attribute mesti ULID yang sah.',
141+
uuid: 'Medan :attribute mesti UUID yang sah.',
142+
143+
/**
144+
* Custom error messages
145+
*/
146+
custom: {
147+
'attribute-name': {
148+
'rule-name': 'custom-message',
149+
},
150+
},
151+
152+
/**
153+
* Custom attributes
154+
*/
155+
attributes: {},
156+
157+
/**
158+
* Custom values
159+
*/
160+
values: {},
161+
};

0 commit comments

Comments
 (0)
Please sign in to comment.