@rbl-dev/validator-ts 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rbl
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 all
13
+ 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 THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # @rbl/validator-ts
2
+
3
+ [![CI](https://github.com)](https://github.com)
4
+ [![npm version](https://shields.io)](https://npmjs.com)
5
+ [![License: MIT](https://shields.io)](https://opensource.org)
6
+
7
+ A lightweight, class-based, extensible validation engine for TypeScript applications that mirrors Laravel's native validation architecture. Stop rewriting form validation logic across the stack—reuse your standard pipe-syntax schemas (`required|email|min:8`) natively inside frontend user interfaces.
8
+
9
+ ## ✨ Features
10
+
11
+ - **Pipe Syntax String Parsing:** Supports classic rule definitions like `required|email|min:6|max:24`.
12
+ - **Compatible Lifecycle API:** Exposes familiar helpers like `.passes()`, `.fails()`, `.invalid()`, and `.getError()`.
13
+ - **Advanced Engine Features:** Bundled out-of-the-box support for validation short-circuiting (`bail`), implicit validation rule skipping, and tokenized custom error message overrides (`:attribute`).
14
+ - **Framework Reactivity Integrations:** Dedicated, optimized reactive hooks for both **React** and **Vue 3**.
15
+ - **Highly Extensible:** Write custom rule classes implementing a standard structural contract or register global shortcuts dynamically via `Validator.extend()`.
16
+
17
+ ---
18
+
19
+ ## 📦 Installation
20
+
21
+ Install the package via your preferred node package manager:
22
+
23
+ ```bash
24
+ npm install @rbl/validator-ts
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 🚀 Basic Core Usage
30
+
31
+ You can use the standalone validation engine anywhere in your TypeScript or Node.js environment exactly like Laravel's backend implementation:
32
+
33
+ ```typescript
34
+ import { Validator } from '@rbl/validator-ts';
35
+
36
+ const data = {
37
+ email: 'not-an-email',
38
+ password: '123',
39
+ };
40
+
41
+ const rules = {
42
+ email: 'bail|required|email',
43
+ password: 'required|min:8',
44
+ };
45
+
46
+ // Custom messages with token placement overrides
47
+ const customMessages = {
48
+ 'email.required': 'We absolutely require an email address.',
49
+ 'password.min': 'Your chosen password is insecure! It must be at least :min characters.',
50
+ };
51
+
52
+ // 1. Core instantiation using the static make factory
53
+ const validator = Validator.make(data, rules, customMessages);
54
+
55
+ // 2. Evaluate outcomes using structural flags
56
+ if (validator.fails()) {
57
+ console.log(validator.invalid());
58
+ // Output: ['email', 'password']
59
+
60
+ console.log(validator.getError('email'));
61
+ // Output: ['The email must be a valid email address.']
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🎨 Frontend Framework Integrations
68
+
69
+ ### 1. React Integration (`useReactFormValidator`)
70
+
71
+ The React hook tracks state using immutable updates. It auto-evaluates on input cycles via a performance-optimized `useMemo` block.
72
+
73
+ ```tsx
74
+ import React from 'react';
75
+ import { useReactFormValidator } from '@rbl/validator-ts';
76
+
77
+ export function RegistrationForm() {
78
+ const { form, handleChange, getError, passes } = useReactFormValidator(
79
+ { email: '', password: '' },
80
+ { email: 'bail|required|email', password: 'required|min:8' },
81
+ { 'email.required': 'Please enter your email address to register.' }
82
+ );
83
+
84
+ const handleSubmit = (e: React.FormEvent) => {
85
+ e.preventDefault();
86
+ if (passes) {
87
+ console.log('Validation success! Sending payload:', form);
88
+ }
89
+ };
90
+
91
+ return (
92
+ <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: '300px' }}>
93
+ <div>
94
+ <label>Email</label>
95
+ <input
96
+ type="text"
97
+ value={form.email}
98
+ onChange={(e) => handleChange('email', e.target.value)}
99
+ />
100
+ {getError('email') && <p style={{ color: 'red', margin: 0 }}>{getError('email')[0]}</p>}
101
+ </div>
102
+
103
+ <div>
104
+ <label>Password</label>
105
+ <input
106
+ type="password"
107
+ value={form.password}
108
+ onChange={(e) => handleChange('password', e.target.value)}
109
+ />
110
+ {getError('password') && <p style={{ color: 'red', margin: 0 }}>{getError('password')[0]}</p>}
111
+ </div>
112
+
113
+ <button type="submit" disabled={!passes}>Register</button>
114
+ </form>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ### 2. Vue 3 Integration (`useVueFormValidator`)
120
+
121
+ The Vue integration hooks into Vue's native proxy reactivity system via `reactive` states and `computed` properties, validating input fields instantly on every keystroke.
122
+
123
+ ```html
124
+ <script setup lang="ts">
125
+ import { useVueFormValidator } from '@rbl/validator-ts';
126
+
127
+ const { form, getError, passes } = useVueFormValidator(
128
+ { email: '', password: '' },
129
+ { email: 'bail|required|email', password: 'required|min:8' }
130
+ );
131
+
132
+ const submitForm = () => {
133
+ if (passes.value) {
134
+ console.log('Validation success! Sending payload:', form);
135
+ }
136
+ };
137
+ </script>
138
+
139
+ <template>
140
+ <form @submit.prevent="submitForm" class="form-container">
141
+ <div class="field">
142
+ <label>Email Address</label>
143
+ <input v-model="form.email" type="text" />
144
+ <span v-if="getError('email')" class="error-msg">{{ getError('email')[0] }}</span>
145
+ </div>
146
+
147
+ <div class="field">
148
+ <label>Password</label>
149
+ <input v-model="form.password" type="password" />
150
+ <span v-if="getError('password')" class="error-msg">{{ getError('password')[0] }}</span>
151
+ </div>
152
+
153
+ <button type="submit" :disabled="!passes">Submit Form</button>
154
+ </form>
155
+ </template>
156
+
157
+ <style scoped>
158
+ .form-container { display: flex; flex-direction: column; gap: 1rem; max-width: 300px; }
159
+ .error-msg { color: red; font-size: 0.85rem; display: block; }
160
+ </style>
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 🛠 Adding Custom Rules
166
+
167
+ ### Method A: Global Extensions (String Shortcuts)
168
+ Register global closures inside your app initialization file to use them anywhere in your string chains:
169
+
170
+ ```typescript
171
+ import { Validator } from '@rbl/validator-ts';
172
+
173
+ // Example: Registering a custom slug format string shortcut rule
174
+ Validator.extend('slug', () => {
175
+ return {
176
+ validate(attribute, value, fail) {
177
+ const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
178
+ if (typeof value === 'string' && !slugPattern.test(value)) {
179
+ fail('The :attribute field must be a valid url slug format.');
180
+ }
181
+ }
182
+ };
183
+ });
184
+
185
+ // Usage
186
+ const rules = { project_name: 'required|slug' };
187
+ ```
188
+
189
+ ### Method B: Instantiated Contract Rule Classes
190
+ For complex rules (like domain-specific checks), create a custom class implementing the `ValidationRule` blueprint contract:
191
+
192
+ ```typescript
193
+ import { ValidationRule, FailCallback } from '@rbl/validator-ts';
194
+
195
+ export class ProhibitDomain implements ValidationRule {
196
+ constructor(protected bannedDomain: string) {}
197
+
198
+ public validate(attribute: string, value: unknown, fail: FailCallback): void {
199
+ if (typeof value === 'string' && value.endsWith(`@${this.bannedDomain}`)) {
200
+ fail(`The :attribute cannot use email addresses registered under ${this.bannedDomain}.`);
201
+ }
202
+ }
203
+ }
204
+
205
+ // Usage
206
+ import { ProhibitDomain } from './rules/ProhibitDomain';
207
+
208
+ const rules = {
209
+ email: ['required', new ProhibitDomain('competitor.com')]
210
+ };
211
+ ```
212
+ ### Method C: Asynchronous Custom Rules (e.g., Database/API Lookups)
213
+ For rules that require server interaction—such as checking if an email is unique—implement the `ValidationRule` contract using an `async/await` signature. The engine natively coordinates async promises concurrently:
214
+
215
+ ```typescript
216
+ import { ValidationRule, FailCallback } from '@rbl/validator-ts';
217
+
218
+ export class UniqueEmailRule implements ValidationRule {
219
+ // Simulating an asynchronous database verification query
220
+ private async checkDatabaseAvailability(email: string): Promise<boolean> {
221
+ return new Promise((resolve) => {
222
+ setTimeout(() => {
223
+ const registeredEmails = ['admin@site.com', 'hello@rbl.dev'];
224
+ resolve(!registeredEmails.includes(email));
225
+ }, 300); // 300ms network delay simulation
226
+ });
227
+ }
228
+
229
+ public async validate(attribute: string, value: unknown, fail: FailCallback): Promise<void> {
230
+ if (typeof value !== 'string') return;
231
+
232
+ const isAvailable = await this.checkDatabaseAvailability(value);
233
+ if (!isAvailable) {
234
+ fail(`The :attribute has already been taken.`);
235
+ }
236
+ }
237
+ }
238
+
239
+ // Usage with Async Engine Setup
240
+ const validator = Validator.make(
241
+ { email: 'hello@rbl.dev' },
242
+ { email: ['required', 'email', new UniqueEmailRule()] }
243
+ );
244
+
245
+ // Trigger the async processing sequence
246
+ await validator.validate();
247
+
248
+ if (validator.fails()) {
249
+ console.log(validator.getError('email'));
250
+ // Output: ['The email has already been taken.']
251
+ }
252
+ ```
253
+
254
+ ## 🚀 Full-Stack Usage Examples
255
+
256
+ ### 1. Node.js Backend API Route (Express Example)
257
+
258
+ Because the core engine supports asynchronous checks, you can use `@rbl/validator-ts` to secure your backend API endpoints. This example shows how to validate incoming request data, use an asynchronous database check, and return structured errors exactly like a Laravel API controller:
259
+
260
+ ```typescript
261
+ import express, { Request, Response } from 'express';
262
+ import { Validator } from '@rbl/validator-ts';
263
+ import { UniqueEmailRule } from './rules/UniqueEmailRule'; // Your custom async rule
264
+
265
+ const app = express();
266
+ app.use(express.json());
267
+
268
+ app.post('/api/register', async (req: Request, res: Response) => {
269
+ // 1. Define your full-stack shared validation rules layout
270
+ const rules = {
271
+ 'user.profile.username': 'bail|required|min:3',
272
+ 'email': ['required', 'email', new UniqueEmailRule()],
273
+ 'password': 'required|min:8'
274
+ };
275
+
276
+ // 2. Custom error messages with dynamic token replacement attributes
277
+ const customMessages = {
278
+ 'user.profile.username.min': 'The username must be at least :min characters long.',
279
+ 'email.required': 'An email address is strictly required to sign up.'
280
+ };
281
+
282
+ // 3. Initialize the style validation instance container
283
+ const validator = Validator.make(req.body, rules, customMessages);
284
+
285
+ // 4. Trigger the asynchronous verification pipeline sequence
286
+ await validator.validate();
287
+
288
+ // 5. Intercept failures and return an immediate 422 Unprocessable Entity payload
289
+ if (validator.fails()) {
290
+ return res.status(422).json({
291
+ message: 'The given data was invalid.',
292
+ errors: validator.getErrors() // Returns standard structured error maps
293
+ });
294
+ }
295
+
296
+ // 6. Execution proceeds safely if all validation parameters pass
297
+ try {
298
+ // Process user registration model database inserts here...
299
+ return res.status(201).json({ message: 'User registered successfully!' });
300
+ } catch (error) {
301
+ return res.status(500).json({ error: 'Internal Server Error' });
302
+ }
303
+ });
304
+
305
+ app.listen(3000, () => console.log('API Server running on port 3000'));
306
+ ```
307
+
308
+ ---
309
+
310
+ ### 2. Standalone Synchronous Usage
311
+
312
+ If you are running a script or validating data locally on the fly without complex asynchronous database checks, use the instant execution method:
313
+
314
+ ```typescript
315
+ import { Validator } from '@rbl/validator-ts';
316
+
317
+ const localData = { username: 'jo' };
318
+ const localRules = { username: 'required|min:5' };
319
+
320
+ const validator = Validator.make(localData, localRules);
321
+
322
+ // 🚀 Execute calculations immediately without using async/await keywords
323
+ validator.validateSync();
324
+
325
+ if (validator.fails()) {
326
+ console.log(validator.getError('username'));
327
+ // Output: ['The username must be at least 5 characters.']
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## 📜 License
334
+
335
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,26 @@
1
+ import { CustomRuleClosureFactory, ValidationRule } from './contracts/Validation.js';
2
+ export declare class Validator {
3
+ private data;
4
+ private customMessages;
5
+ private errors;
6
+ private normalizedRules;
7
+ private static extensions;
8
+ constructor(data: Record<string, unknown>, rules: Record<string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>);
9
+ static make(data: Record<string, unknown>, rules: Record<string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>): Validator;
10
+ static extend(ruleName: string, factory: CustomRuleClosureFactory): void;
11
+ private normalizeRules;
12
+ private run;
13
+ validate(): Promise<boolean>;
14
+ /**
15
+ * Run the validation process synchronously across all rules.
16
+ * Automatically skips any complex asynchronous rule objects.
17
+ */
18
+ validateSync(): boolean;
19
+ private resolveStringRule;
20
+ private addError;
21
+ passes(): boolean;
22
+ fails(): boolean;
23
+ invalid(): string[];
24
+ getError(name: string): string[] | null;
25
+ getErrors(): Record<string, string[]>;
26
+ }
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Validator = void 0;
4
+ const EmailRule_js_1 = require("./rules/EmailRule.js");
5
+ const MaxRule_js_1 = require("./rules/MaxRule.js");
6
+ const MinRule_js_1 = require("./rules/MinRule.js");
7
+ const RequiredRule_js_1 = require("./rules/RequiredRule.js");
8
+ const data_1 = require("./utils/data");
9
+ class Validator {
10
+ data;
11
+ customMessages;
12
+ errors = {};
13
+ normalizedRules = {};
14
+ static extensions = {};
15
+ constructor(data, rules, customMessages = {}) {
16
+ this.data = data;
17
+ this.customMessages = customMessages;
18
+ this.normalizeRules(rules);
19
+ this.run();
20
+ }
21
+ static make(data, rules, customMessages = {}) {
22
+ return new Validator(data, rules, customMessages);
23
+ }
24
+ static extend(ruleName, factory) {
25
+ Validator.extensions[ruleName] = factory;
26
+ }
27
+ normalizeRules(rules) {
28
+ for (const [attribute, ruleMix] of Object.entries(rules)) {
29
+ let ruleArray = [];
30
+ if (typeof ruleMix === 'string') {
31
+ ruleArray = ruleMix.split('|');
32
+ }
33
+ else {
34
+ for (const item of ruleMix) {
35
+ if (typeof item === 'string') {
36
+ ruleArray.push(...item.split('|'));
37
+ }
38
+ else {
39
+ ruleArray.push(item);
40
+ }
41
+ }
42
+ }
43
+ this.normalizedRules[attribute] = ruleArray;
44
+ }
45
+ }
46
+ run() {
47
+ this.errors = {};
48
+ for (const [attribute, attributeRules] of Object.entries(this.normalizedRules)) {
49
+ const value = this.data[attribute];
50
+ const isEmpty = value === undefined || value === null || value === '';
51
+ let shouldBail = false;
52
+ for (const rawRule of attributeRules) {
53
+ if (rawRule === 'bail') {
54
+ shouldBail = true;
55
+ continue;
56
+ }
57
+ const rule = typeof rawRule === 'string'
58
+ ? this.resolveStringRule(rawRule)
59
+ : rawRule;
60
+ if (!rule)
61
+ continue;
62
+ const isImplicit = rule.isImplicit === true;
63
+ if (isEmpty && !isImplicit)
64
+ continue;
65
+ let ruleFailed = false;
66
+ const ruleName = typeof rawRule === 'string'
67
+ ? rawRule.split(':')[0]
68
+ : rule.constructor.name
69
+ .toLowerCase()
70
+ .replace('rule', '');
71
+ rule.validate(attribute, value, (defaultMessage) => {
72
+ ruleFailed = true;
73
+ const customKeyWithAttr = `${attribute}.${ruleName}`;
74
+ const finalMessagePattern = this.customMessages[customKeyWithAttr] ||
75
+ this.customMessages[ruleName] ||
76
+ defaultMessage;
77
+ const localizedMessage = finalMessagePattern.replace(/:attribute/g, attribute);
78
+ this.addError(attribute, localizedMessage);
79
+ });
80
+ if (shouldBail && ruleFailed)
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ async validate() {
86
+ this.errors = {};
87
+ const validationPromises = [];
88
+ for (const [attribute, attributeRules] of Object.entries(this.normalizedRules)) {
89
+ // 🚀 Fix: Dynamically locate deeply nested object keys using dot notation
90
+ const value = (0, data_1.dataGet)(this.data, attribute);
91
+ const isEmpty = value === undefined || value === null || value === '';
92
+ // We handle bails in an isolation chain block per field
93
+ validationPromises.push((async () => {
94
+ let shouldBail = false;
95
+ for (const rawRule of attributeRules) {
96
+ if (rawRule === 'bail') {
97
+ shouldBail = true;
98
+ continue;
99
+ }
100
+ const rule = typeof rawRule === 'string'
101
+ ? this.resolveStringRule(rawRule)
102
+ : rawRule;
103
+ if (!rule)
104
+ continue;
105
+ const isImplicit = rule.isImplicit === true;
106
+ if (isEmpty && !isImplicit)
107
+ continue;
108
+ let ruleFailed = false;
109
+ const ruleName = typeof rawRule === 'string'
110
+ ? rawRule.split(':')[0]
111
+ : rule.constructor.name
112
+ .toLowerCase()
113
+ .replace('rule', '');
114
+ // Wrap execution safely inside a Promise to manage potential async rules
115
+ await Promise.resolve(rule.validate(attribute, value, (defaultMessage) => {
116
+ ruleFailed = true;
117
+ const customKeyWithAttr = `${attribute}.${ruleName}`;
118
+ const finalMessagePattern = this.customMessages[customKeyWithAttr] ||
119
+ this.customMessages[ruleName] ||
120
+ defaultMessage;
121
+ const localizedMessage = finalMessagePattern.replace(/:attribute/g, attribute);
122
+ this.addError(attribute, localizedMessage);
123
+ }));
124
+ if (shouldBail && ruleFailed)
125
+ break;
126
+ }
127
+ })());
128
+ }
129
+ // Await execution blocks concurrently across all fields
130
+ await Promise.all(validationPromises);
131
+ return this.passes();
132
+ }
133
+ /**
134
+ * Run the validation process synchronously across all rules.
135
+ * Automatically skips any complex asynchronous rule objects.
136
+ */
137
+ validateSync() {
138
+ this.errors = {};
139
+ for (const [attribute, attributeRules] of Object.entries(this.normalizedRules)) {
140
+ const value = (0, data_1.dataGet)(this.data, attribute);
141
+ const isEmpty = value === undefined || value === null || value === '';
142
+ let shouldBail = false;
143
+ for (const rawRule of attributeRules) {
144
+ if (rawRule === 'bail') {
145
+ shouldBail = true;
146
+ continue;
147
+ }
148
+ const rule = typeof rawRule === 'string'
149
+ ? this.resolveStringRule(rawRule)
150
+ : rawRule;
151
+ if (!rule)
152
+ continue;
153
+ // Skip async promise rule instances safely in synchronous mode
154
+ if (
155
+ //@ts-ignore
156
+ typeof rule.validate(attribute, value, () => { })?.then ===
157
+ 'function') {
158
+ continue;
159
+ }
160
+ const isImplicit = rule.isImplicit === true;
161
+ if (isEmpty && !isImplicit)
162
+ continue;
163
+ let ruleFailed = false;
164
+ const ruleName = typeof rawRule === 'string'
165
+ ? rawRule.split(':')[0]
166
+ : rule.constructor.name
167
+ .toLowerCase()
168
+ .replace('rule', '');
169
+ rule.validate(attribute, value, (defaultMessage) => {
170
+ ruleFailed = true;
171
+ const customKeyWithAttr = `${attribute}.${ruleName}`;
172
+ const finalMessagePattern = this.customMessages[customKeyWithAttr] ||
173
+ this.customMessages[ruleName] ||
174
+ defaultMessage;
175
+ const localizedMessage = finalMessagePattern.replace(/:attribute/g, attribute);
176
+ this.addError(attribute, localizedMessage);
177
+ });
178
+ if (shouldBail && ruleFailed)
179
+ break;
180
+ }
181
+ }
182
+ return this.passes();
183
+ }
184
+ resolveStringRule(ruleString) {
185
+ const [ruleName, ...paramParts] = ruleString.split(':');
186
+ const paramString = paramParts.join(':');
187
+ const params = paramString ? paramString.split(',') : [];
188
+ if (ruleName === 'required')
189
+ return new RequiredRule_js_1.RequiredRule();
190
+ if (ruleName === 'email')
191
+ return new EmailRule_js_1.EmailRule();
192
+ if (ruleName === 'min')
193
+ return new MinRule_js_1.MinRule(parseInt(params[0], 10));
194
+ if (ruleName === 'max')
195
+ return new MaxRule_js_1.MaxRule(parseInt(params[0], 10));
196
+ if (Validator.extensions[ruleName]) {
197
+ return Validator.extensions[ruleName](params);
198
+ }
199
+ throw new Error(`Validation rule "${ruleName}" is not registered.`);
200
+ }
201
+ addError(attribute, message) {
202
+ if (!this.errors[attribute])
203
+ this.errors[attribute] = [];
204
+ this.errors[attribute].push(message);
205
+ }
206
+ passes() {
207
+ return Object.keys(this.errors).length === 0;
208
+ }
209
+ fails() {
210
+ return !this.passes();
211
+ }
212
+ invalid() {
213
+ return Object.keys(this.errors);
214
+ }
215
+ getError(name) {
216
+ return this.errors[name] ? this.errors[name] : null;
217
+ }
218
+ getErrors() {
219
+ return this.errors;
220
+ }
221
+ }
222
+ exports.Validator = Validator;
@@ -0,0 +1,5 @@
1
+ export type FailCallback = (message: string) => void;
2
+ export interface ValidationRule {
3
+ validate(attribute: string, value: unknown, fail: FailCallback): void;
4
+ }
5
+ export type CustomRuleClosureFactory = (params: string[]) => ValidationRule;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ export * from './contracts/Validation';
2
+ export * from './rules/EmailRule';
3
+ export * from './rules/MaxRule';
4
+ export * from './rules/MinRule';
5
+ export * from './rules/RequiredRule';
6
+ export * from './Validator';
7
+ export * from './integrations/react';
8
+ export * from './integrations/vue';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./contracts/Validation"), exports);
18
+ __exportStar(require("./rules/EmailRule"), exports);
19
+ __exportStar(require("./rules/MaxRule"), exports);
20
+ __exportStar(require("./rules/MinRule"), exports);
21
+ __exportStar(require("./rules/RequiredRule"), exports);
22
+ __exportStar(require("./Validator"), exports);
23
+ __exportStar(require("./integrations/react"), exports);
24
+ __exportStar(require("./integrations/vue"), exports);
@@ -0,0 +1,29 @@
1
+ import { ValidationRule } from '../contracts/Validation';
2
+ /**
3
+ * SYNCHRONOUS HOOK: Instant keystroke validation
4
+ * Best for fast local layout constraints (min, max, email format, required)
5
+ */
6
+ export declare function useReactFormValidator<T extends Record<string, unknown>>(initialData: T, rules: Record<keyof T | string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>): {
7
+ form: T;
8
+ setForm: import("react").Dispatch<import("react").SetStateAction<T>>;
9
+ handleChange: (field: keyof T, value: unknown) => void;
10
+ errors: Record<string, string[]>;
11
+ passes: boolean;
12
+ fails: boolean;
13
+ getError: (field: keyof T | string) => string[];
14
+ };
15
+ /**
16
+ * ASYNCHRONOUS HOOK: Explicit / Managed Lifecycle Validation
17
+ * Best for network checks (unique email lookup, coupon codes, server validations)
18
+ */
19
+ export declare function useReactAsyncFormValidator<T extends Record<string, unknown>>(initialData: T, rules: Record<keyof T | string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>): {
20
+ form: T;
21
+ setForm: import("react").Dispatch<import("react").SetStateAction<T>>;
22
+ handleChange: (field: keyof T, value: unknown) => void;
23
+ validateAsync: (currentData?: T) => Promise<boolean>;
24
+ errors: Record<string, string[]>;
25
+ isPending: boolean;
26
+ passes: boolean;
27
+ fails: boolean;
28
+ getError: (field: keyof T | string) => string[];
29
+ };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useReactFormValidator = useReactFormValidator;
4
+ exports.useReactAsyncFormValidator = useReactAsyncFormValidator;
5
+ const react_1 = require("react");
6
+ const Validator_1 = require("../Validator");
7
+ /**
8
+ * SYNCHRONOUS HOOK: Instant keystroke validation
9
+ * Best for fast local layout constraints (min, max, email format, required)
10
+ */
11
+ function useReactFormValidator(initialData, rules, customMessages = {}) {
12
+ const [form, setForm] = (0, react_1.useState)(initialData);
13
+ // Run synchronously via useMemo so there is zero rendering delay
14
+ const validator = (0, react_1.useMemo)(() => {
15
+ const v = Validator_1.Validator.make(form, rules, customMessages);
16
+ // Explicitly runs sync rules; skips async lookups seamlessly on keystroke
17
+ v.validateSync();
18
+ return v;
19
+ }, [form, rules, customMessages]);
20
+ const handleChange = (0, react_1.useCallback)((field, value) => {
21
+ setForm((prev) => ({ ...prev, [field]: value }));
22
+ }, []);
23
+ return {
24
+ form,
25
+ setForm,
26
+ handleChange,
27
+ errors: validator.getErrors(),
28
+ passes: validator.passes(),
29
+ fails: validator.fails(),
30
+ getError: (field) => validator.getErrors()[field] || null,
31
+ };
32
+ }
33
+ /**
34
+ * ASYNCHRONOUS HOOK: Explicit / Managed Lifecycle Validation
35
+ * Best for network checks (unique email lookup, coupon codes, server validations)
36
+ */
37
+ function useReactAsyncFormValidator(initialData, rules, customMessages = {}) {
38
+ const [form, setForm] = (0, react_1.useState)(initialData);
39
+ const [errors, setErrors] = (0, react_1.useState)({});
40
+ const [isPending, setIsPending] = (0, react_1.useState)(false);
41
+ const [isValid, setIsValid] = (0, react_1.useState)(true);
42
+ const handleChange = (0, react_1.useCallback)((field, value) => {
43
+ setForm((prev) => ({ ...prev, [field]: value }));
44
+ }, []);
45
+ // Call this explicitly on form submit or input blur events
46
+ const validateAsync = (0, react_1.useCallback)(async (currentData = form) => {
47
+ setIsPending(true);
48
+ const v = Validator_1.Validator.make(currentData, rules, customMessages);
49
+ await v.validate();
50
+ setErrors(v.getErrors());
51
+ setIsValid(v.passes());
52
+ setIsPending(false);
53
+ return v.passes();
54
+ }, [form, rules, customMessages]);
55
+ return {
56
+ form,
57
+ setForm,
58
+ handleChange,
59
+ validateAsync,
60
+ errors,
61
+ isPending,
62
+ passes: isValid,
63
+ fails: !isValid,
64
+ getError: (field) => errors[field] || null,
65
+ };
66
+ }
@@ -0,0 +1,23 @@
1
+ import { ValidationRule } from '../contracts/Validation';
2
+ /**
3
+ *SYNCHRONOUS COMPOSABLE: Instant Keystroke Validation
4
+ */
5
+ export declare function useVueFormValidator<T extends Record<string, unknown>>(initialData: T, rules: Record<keyof T | string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>): {
6
+ form: import("vue").Reactive<T>;
7
+ errors: import("vue").ComputedRef<Record<string, string[]>>;
8
+ passes: import("vue").ComputedRef<boolean>;
9
+ fails: import("vue").ComputedRef<boolean>;
10
+ getError: (field: keyof T | string) => string[];
11
+ };
12
+ /**
13
+ * ASYNCHRONOUS COMPOSABLE: Controlled Server Validation
14
+ */
15
+ export declare function useVueAsyncFormValidator<T extends Record<string, unknown>>(initialData: T, rules: Record<keyof T | string, string | (string | ValidationRule)[]>, customMessages?: Record<string, string>): {
16
+ form: import("vue").Reactive<T>;
17
+ validateAsync: () => Promise<boolean>;
18
+ errors: import("vue").Ref<Record<string, string[]>, Record<string, string[]>>;
19
+ isPending: import("vue").Ref<boolean, boolean>;
20
+ passes: import("vue").ComputedRef<boolean>;
21
+ fails: import("vue").ComputedRef<boolean>;
22
+ getError: (field: keyof T | string) => string[];
23
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useVueFormValidator = useVueFormValidator;
4
+ exports.useVueAsyncFormValidator = useVueAsyncFormValidator;
5
+ const vue_1 = require("vue");
6
+ const Validator_1 = require("../Validator");
7
+ /**
8
+ *SYNCHRONOUS COMPOSABLE: Instant Keystroke Validation
9
+ */
10
+ function useVueFormValidator(initialData, rules, customMessages = {}) {
11
+ const form = (0, vue_1.reactive)(initialData);
12
+ const validator = (0, vue_1.computed)(() => {
13
+ const v = Validator_1.Validator.make(form, rules, customMessages);
14
+ v.validateSync(); // 🚀 Uses the instant, synchronous evaluation loop!
15
+ return v;
16
+ });
17
+ return {
18
+ form,
19
+ errors: (0, vue_1.computed)(() => validator.value.getErrors()),
20
+ passes: (0, vue_1.computed)(() => validator.value.passes()),
21
+ fails: (0, vue_1.computed)(() => validator.value.fails()),
22
+ getError: (field) => validator.value.getErrors()[field] || null,
23
+ };
24
+ }
25
+ /**
26
+ * ASYNCHRONOUS COMPOSABLE: Controlled Server Validation
27
+ */
28
+ function useVueAsyncFormValidator(initialData, rules, customMessages = {}) {
29
+ const form = (0, vue_1.reactive)(initialData);
30
+ const errors = (0, vue_1.ref)({});
31
+ const isPending = (0, vue_1.ref)(false);
32
+ const isValid = (0, vue_1.ref)(true);
33
+ // Controlled execution trigger
34
+ const validateAsync = async () => {
35
+ isPending.value = true;
36
+ const v = Validator_1.Validator.make(form, rules, customMessages);
37
+ await v.validate();
38
+ errors.value = v.getErrors();
39
+ isValid.value = v.passes();
40
+ isPending.value = false;
41
+ return v.passes();
42
+ };
43
+ return {
44
+ form,
45
+ validateAsync,
46
+ errors,
47
+ isPending,
48
+ passes: (0, vue_1.computed)(() => isValid.value),
49
+ fails: (0, vue_1.computed)(() => !isValid.value),
50
+ getError: (field) => errors.value[field] || null,
51
+ };
52
+ }
@@ -0,0 +1,4 @@
1
+ import { FailCallback, ValidationRule } from '../contracts/Validation';
2
+ export declare class EmailRule implements ValidationRule {
3
+ validate(attr: string, val: unknown, fail: FailCallback): void;
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmailRule = void 0;
4
+ class EmailRule {
5
+ validate(attr, val, fail) {
6
+ if (typeof val === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val))
7
+ fail(`The :attribute must be a valid email address.`);
8
+ }
9
+ }
10
+ exports.EmailRule = EmailRule;
@@ -0,0 +1,6 @@
1
+ import { FailCallback, ValidationRule } from '../contracts/Validation';
2
+ export declare class MaxRule implements ValidationRule {
3
+ private max;
4
+ constructor(max: number);
5
+ validate(attr: string, val: unknown, fail: FailCallback): void;
6
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MaxRule = void 0;
4
+ class MaxRule {
5
+ max;
6
+ constructor(max) {
7
+ this.max = max;
8
+ }
9
+ validate(attr, val, fail) {
10
+ if (typeof val === 'string' && val.length > this.max)
11
+ fail(`The :attribute may not be greater than ${this.max} characters.`);
12
+ }
13
+ }
14
+ exports.MaxRule = MaxRule;
@@ -0,0 +1,6 @@
1
+ import { FailCallback, ValidationRule } from '../contracts/Validation';
2
+ export declare class MinRule implements ValidationRule {
3
+ private min;
4
+ constructor(min: number);
5
+ validate(attr: string, val: unknown, fail: FailCallback): void;
6
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MinRule = void 0;
4
+ class MinRule {
5
+ min;
6
+ constructor(min) {
7
+ this.min = min;
8
+ }
9
+ validate(attr, val, fail) {
10
+ if (typeof val === 'string' && val.length < this.min)
11
+ fail(`The :attribute must be at least ${this.min} characters.`);
12
+ }
13
+ }
14
+ exports.MinRule = MinRule;
@@ -0,0 +1,6 @@
1
+ import { FailCallback, ValidationRule } from '../contracts/Validation';
2
+ /** Built-In Core Rule Fallbacks */
3
+ export declare class RequiredRule implements ValidationRule {
4
+ isImplicit: boolean;
5
+ validate(attr: string, val: unknown, fail: FailCallback): void;
6
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RequiredRule = void 0;
4
+ /** Built-In Core Rule Fallbacks */
5
+ class RequiredRule {
6
+ isImplicit = true;
7
+ validate(attr, val, fail) {
8
+ if (val === undefined || val === null || val === '')
9
+ fail(`The :attribute field is required.`);
10
+ }
11
+ }
12
+ exports.RequiredRule = RequiredRule;
@@ -0,0 +1,5 @@
1
+ import { FailCallback, ValidationRule } from '../contracts/Validation';
2
+ export declare class UniqueEmailRule implements ValidationRule {
3
+ private checkDatabaseAvailability;
4
+ validate(attribute: string, value: unknown, fail: FailCallback): Promise<void>;
5
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UniqueEmailRule = void 0;
4
+ class UniqueEmailRule {
5
+ // Simulate an async data call / API lookup query
6
+ async checkDatabaseAvailability(email) {
7
+ return new Promise((resolve) => {
8
+ setTimeout(() => {
9
+ const takenEmails = ['admin@site.com', 'hello@rbl.dev'];
10
+ resolve(!takenEmails.includes(email));
11
+ }, 400); // 400ms server delay simulation
12
+ });
13
+ }
14
+ async validate(attribute, value, fail) {
15
+ if (typeof value !== 'string')
16
+ return;
17
+ const isAvailable = await this.checkDatabaseAvailability(value);
18
+ if (!isAvailable) {
19
+ fail(`The :attribute has already been taken.`);
20
+ }
21
+ }
22
+ }
23
+ exports.UniqueEmailRule = UniqueEmailRule;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Safely extracts a value from a deeply nested object using dot-notation.
3
+ * Example: dataGet({ user: { age: 25 } }, 'user.age') -> 25
4
+ */
5
+ export declare function dataGet(target: Record<string, unknown>, path: string): unknown;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dataGet = dataGet;
4
+ /**
5
+ * Safely extracts a value from a deeply nested object using dot-notation.
6
+ * Example: dataGet({ user: { age: 25 } }, 'user.age') -> 25
7
+ */
8
+ function dataGet(target, path) {
9
+ return path.split('.').reduce((accumulator, key) => {
10
+ if (accumulator &&
11
+ typeof accumulator === 'object' &&
12
+ key in accumulator) {
13
+ return accumulator[key];
14
+ }
15
+ return undefined;
16
+ }, target);
17
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@rbl-dev/validator-ts",
3
+ "version": "1.0.0",
4
+ "description": "A Laravel-inspired class-based pipe validator for TypeScript applications.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "clean": "rimraf dist",
15
+ "prebuild": "npm run clean",
16
+ "build": "tsc",
17
+ "test": "vitest run"
18
+ },
19
+ "keywords": [
20
+ "laravel",
21
+ "validator",
22
+ "validation",
23
+ "typescript",
24
+ "react",
25
+ "vue"
26
+ ],
27
+ "author": "Ruel B. Lapid",
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "react": "^18.0.0 || ^19.0.0",
31
+ "vue": "^3.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "react": {
35
+ "optional": true
36
+ },
37
+ "vue": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@testing-library/react": "^16.3.2",
43
+ "@types/react": "^19.2.15",
44
+ "happy-dom": "^20.9.0",
45
+ "react": "^19.0.0",
46
+ "rimraf": "^5.0.0",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^4.1.7",
49
+ "vue": "^3.5.0"
50
+ }
51
+ }