├── .gitignore ├── tests ├── test_suite │ ├── negative │ │ ├── has_methods │ │ │ ├── samples │ │ │ │ └── Cat.js │ │ │ ├── input.js │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── instance_of │ │ │ ├── samples │ │ │ │ ├── Cat.js │ │ │ │ └── Dog.js │ │ │ ├── errors.json │ │ │ ├── input.js │ │ │ └── rules.js │ │ ├── required_if │ │ │ ├── errors.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── md5 │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ └── input.json │ │ ├── boolean │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ ├── base64 │ │ │ ├── rules.json │ │ │ ├── input.json │ │ │ └── errors.json │ │ ├── mongo_id │ │ │ ├── errors.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── is │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ ├── ipv4 │ │ │ ├── errors.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── uuid │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ └── input.json │ │ ├── list_items_unique │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── list_length │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── credit_card │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ └── iso_date │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ └── positive │ │ ├── instance_of │ │ ├── samples │ │ │ ├── Animal.js │ │ │ └── Dog.js │ │ ├── output.js │ │ ├── input.js │ │ └── rules.js │ │ ├── md5 │ │ ├── rules.json │ │ ├── output.json │ │ └── input.json │ │ ├── is │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json │ │ ├── has_methods │ │ ├── samples │ │ │ ├── Animal.js │ │ │ └── Dog.js │ │ ├── output.js │ │ ├── rules.json │ │ └── input.js │ │ ├── ipv4 │ │ ├── output.json │ │ ├── rules.json │ │ └── input.json │ │ ├── base64 │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json │ │ ├── credit_card │ │ ├── input.json │ │ ├── output.json │ │ └── rules.json │ │ ├── list_items_unique │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json │ │ ├── mongo_id │ │ ├── rules.json │ │ ├── output.json │ │ └── input.json │ │ ├── list_length │ │ ├── rules.json │ │ ├── output.json │ │ └── input.json │ │ ├── boolean │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json │ │ ├── uuid │ │ ├── rules.json │ │ ├── output.json │ │ └── input.json │ │ ├── required_if │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json │ │ └── iso_date │ │ ├── output.json │ │ ├── input.json │ │ └── rules.json └── test_suite.js ├── .prettierrc.json ├── .travis.yml ├── src ├── rules │ ├── mongo_id.js │ ├── md5.js │ ├── is.js │ ├── instance_of.js │ ├── boolean.js │ ├── base64.js │ ├── ipv4.js │ ├── list_items_unique.js │ ├── has_methods.js │ ├── list_length.js │ ├── required_if.js │ ├── credit_card.js │ ├── uuid.js │ └── iso_date.js ├── index.js └── util.js ├── tsconfig.json ├── CHANGELOG.md ├── LICENSE ├── package.json ├── CLAUDE.md ├── types └── index.d.ts ├── benchmark.js ├── README.md └── test-types.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .vscode -------------------------------------------------------------------------------- /tests/test_suite/negative/has_methods/samples/Cat.js: -------------------------------------------------------------------------------- 1 | module.exports = class Cat {}; -------------------------------------------------------------------------------- /tests/test_suite/negative/instance_of/samples/Cat.js: -------------------------------------------------------------------------------- 1 | module.exports = class Cat {}; -------------------------------------------------------------------------------- /tests/test_suite/negative/instance_of/samples/Dog.js: -------------------------------------------------------------------------------- 1 | module.exports = class Dog {}; -------------------------------------------------------------------------------- /tests/test_suite/positive/instance_of/samples/Animal.js: -------------------------------------------------------------------------------- 1 | module.exports = class Animal {}; -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 100, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '16' 4 | - '14' 5 | - '12' 6 | - '10' 7 | -------------------------------------------------------------------------------- /tests/test_suite/positive/md5/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "md5", 3 | "md5_2": { "md5": [] }, 4 | "empty_field": "md5" 5 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/instance_of/samples/Dog.js: -------------------------------------------------------------------------------- 1 | const Animal = require('./Animal'); 2 | module.exports = class Dog extends Animal {}; -------------------------------------------------------------------------------- /tests/test_suite/positive/is/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": "test", 3 | "is_2": 1, 4 | "is_3": true, 5 | "is_4": "false" 6 | } 7 | -------------------------------------------------------------------------------- /tests/test_suite/positive/has_methods/samples/Animal.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | module.exports = class Animal { 3 | getName() {} 4 | }; -------------------------------------------------------------------------------- /tests/test_suite/negative/required_if/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "required_if_1": "REQUIRED", 3 | "required_if_2": "REQUIRED", 4 | "required_if_3": "REQUIRED" 5 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/ipv4/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "192.168.0.1", 3 | "ipv4_2": "1.1.1.1", 4 | "ipv4_3": "10.1.255.1", 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/is/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": "test", 3 | "is_2": 1, 4 | "is_3": true, 5 | "is_4": false, 6 | "extra_field": "aaaa" 7 | } 8 | -------------------------------------------------------------------------------- /tests/test_suite/positive/base64/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "TWFuIGl=", 3 | "base64_2": "TWFuIGl", 4 | "base64_3": "1234567890", 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/credit_card/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "1548156849712366", 3 | "credit_card_2": 1548156849712366, 4 | "empty_field": "" 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_suite/positive/credit_card/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "1548156849712366", 3 | "credit_card_2": 1548156849712366, 4 | "empty_field": "" 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_suite/positive/credit_card/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "credit_card", 3 | "credit_card_2": "credit_card", 4 | "empty_field": "credit_card" 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_suite/positive/ipv4/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "ipv4", 3 | "ipv4_2": { "ipv4": [] }, 4 | "ipv4_3": [ { "ipv4": [] } ], 5 | "empty_field": "ipv4" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/is/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": {"is": "test"}, 3 | "is_2": {"is": [ 1 ]}, 4 | "is_3": [ {"is": true} ], 5 | "is_4": {"is": "false"} 6 | } 7 | -------------------------------------------------------------------------------- /tests/test_suite/positive/md5/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "1BC29B36F623BA82AAF6724FD3B16718", 3 | "md5_2": "d41d8cd98f00b204e9800998ecf8427e", 4 | "empty_field": "" 5 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/has_methods/samples/Dog.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const Animal = require('./Animal'); 3 | module.exports = class Dog extends Animal { 4 | bark() {} 5 | }; -------------------------------------------------------------------------------- /tests/test_suite/positive/list_items_unique/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", 1, true], 3 | "list_2": ["test1", "test2"], 4 | "list_3": [true, false], 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/base64/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "TWFuIGl=", 3 | "base64_2": "TWFuIGl", 4 | "base64_3": 1234567890, 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/ipv4/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "192.168.0.1", 3 | "ipv4_2": "1.1.1.1", 4 | "ipv4_3": "10.1.255.1", 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/base64/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "base64", 3 | "base64_2": { "base64": "relaxed" }, 4 | "base64_3": { "base64": [ "relaxed" ] }, 5 | "empty_field": "base64" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/mongo_id/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "mongo_id", 3 | "mongo_id_2": { "mongo_id": [] }, 4 | "mongo_id_3": [ { "mongo_id": [] } ], 5 | "empty_field": "mongo_id" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/md5/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "1BC29B36F623BA82AAF6724FD3B16718", 3 | "md5_2": "d41d8cd98f00b204e9800998ecf8427e", 4 | "empty_field": "", 5 | "extra_field": "aaaa" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/has_methods/output.js: -------------------------------------------------------------------------------- 1 | const input = require('./input'); 2 | 3 | module.exports = { 4 | "dog1": input.dog1, 5 | "dog2": input.dog2, 6 | "dog3": input.dog3, 7 | "empty_field": "" 8 | }; -------------------------------------------------------------------------------- /tests/test_suite/positive/instance_of/output.js: -------------------------------------------------------------------------------- 1 | const input = require('./input'); 2 | 3 | module.exports = { 4 | "dog1": input.dog1, 5 | "dog2": input.dog2, 6 | "dog3": input.dog3, 7 | "empty_field": "" 8 | }; -------------------------------------------------------------------------------- /tests/test_suite/positive/list_items_unique/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", 1, true], 3 | "list_2": ["test1", "test2"], 4 | "list_3": [true, false], 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/list_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["required", {"list_length": 7}], 3 | "list_2": {"list_length": 7}, 4 | "list_3": {"list_length": [3, 10] }, 5 | "empty_field": {"list_length": 7} 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/mongo_id/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "5a5622616acdc6684a892c75", 3 | "mongo_id_2": "000000000000000000000000", 4 | "mongo_id_3": "ffffffffffffffffffffffff", 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/list_items_unique/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": "list_items_unique", 3 | "list_2": { "list_items_unique": [] }, 4 | "list_3": [ { "list_items_unique": [] } ], 5 | "empty_field": "list_items_unique" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/has_methods/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "dog1": {"has_methods": "bark"}, 3 | "dog2": {"has_methods": ["getName"]}, 4 | "dog3": {"has_methods": [["bark", "getName"]]}, 5 | "empty_field": {"has_methods": "bark"} 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/boolean/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": true, 3 | "boolean_2": true, 4 | "boolean_3": true, 5 | "boolean_4": false, 6 | "boolean_5": false, 7 | "boolean_6": false, 8 | "empty_field": "" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_suite/positive/has_methods/input.js: -------------------------------------------------------------------------------- 1 | const Dog = require('./samples/Dog'); 2 | 3 | module.exports = { 4 | "dog1": new Dog(), 5 | "dog2": new Dog(), 6 | "dog3": new Dog(), 7 | "empty_field": "", 8 | "extra_field": "aaaa" 9 | }; -------------------------------------------------------------------------------- /tests/test_suite/positive/instance_of/input.js: -------------------------------------------------------------------------------- 1 | const Dog = require('./samples/Dog'); 2 | 3 | module.exports = { 4 | "dog1": new Dog(), 5 | "dog2": new Dog(), 6 | "dog3": new Dog(), 7 | "empty_field": "", 8 | "extra_field": "aaaa" 9 | }; -------------------------------------------------------------------------------- /tests/test_suite/positive/list_length/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", 1, {"1": 1, "2": 2}, 2, [], 3, true], 3 | "list_2": ["test", 1, {}, 2, [10, 20, 30], 3, false], 4 | "list_3": ["test", 1, {}, 2, [], 3, false], 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/mongo_id/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "5a5622616acdc6684a892c75", 3 | "mongo_id_2": "000000000000000000000000", 4 | "mongo_id_3": "ffffffffffffffffffffffff", 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/boolean/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": true, 3 | "boolean_2": 1, 4 | "boolean_3": "1", 5 | "boolean_4": false, 6 | "boolean_5": 0, 7 | "boolean_6": "0", 8 | "empty_field": "", 9 | "extra_field": "aaaa" 10 | } 11 | -------------------------------------------------------------------------------- /tests/test_suite/positive/boolean/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": "boolean", 3 | "boolean_2": "boolean", 4 | "boolean_3": "boolean", 5 | "boolean_4": "boolean", 6 | "boolean_5": "boolean", 7 | "boolean_6": "boolean", 8 | "empty_field": "boolean" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_suite/positive/list_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", 1, {"1": 1, "2": 2}, 2, [], 3, true], 3 | "list_2": ["test", 1, {}, 2, [10, 20, 30], 3, false], 4 | "list_3": ["test", 1, {}, 2, [], 3, false], 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/uuid/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": { "uuid": "v1" }, 3 | "uuid_2": { "uuid": "v2" }, 4 | "uuid_3": { "uuid": "v3" }, 5 | "uuid_4": [ { "uuid": "v4" } ], 6 | "uuid_5": [ { "uuid": "v5" } ], 7 | "uuid_6": "uuid", 8 | "empty_field": "uuid" 9 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/instance_of/rules.js: -------------------------------------------------------------------------------- 1 | const Animal = require('./samples/Animal'); 2 | const Dog = require('./samples/Dog'); 3 | 4 | module.exports = { 5 | "dog1": {"instance_of": Animal}, 6 | "dog2": {"instance_of": Dog}, 7 | "dog3": {"instance_of": [Dog]}, 8 | "empty_field": {"instance_of": Animal}, 9 | }; -------------------------------------------------------------------------------- /tests/test_suite/negative/md5/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "md5", 3 | "md5_2": { "md5": [] }, 4 | "md5_3": [ { "md5": [] } ], 5 | "md5_4": "md5", 6 | 7 | "value_is_hash": "md5", 8 | "value_is_empty_hash": "md5", 9 | "value_is_array": "md5", 10 | "value_is_empty_array": "md5" 11 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/boolean/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": "aaa", 3 | "boolean_2": -1, 4 | "boolean_3": 2, 5 | "extra_field": "aaaa", 6 | 7 | "value_is_hash": {"test": 1}, 8 | "value_is_empty_hash": {}, 9 | "value_is_array": ["test", 1], 10 | "value_is_empty_array": [] 11 | } 12 | -------------------------------------------------------------------------------- /tests/test_suite/negative/boolean/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": "boolean", 3 | "boolean_2": "boolean", 4 | "boolean_3": "boolean", 5 | 6 | "value_is_hash": "boolean", 7 | "value_is_empty_hash": "boolean", 8 | "value_is_array": "boolean", 9 | "value_is_empty_array": "boolean" 10 | } 11 | -------------------------------------------------------------------------------- /tests/test_suite/negative/base64/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "base64", 3 | "base64_2": { "base64": "relaxed" }, 4 | "base64_3": { "base64": [] }, 5 | 6 | "value_is_hash": "base64", 7 | "value_is_empty_hash": "base64", 8 | "value_is_array": "base64", 9 | "value_is_empty_array": "base64" 10 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/base64/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "TWFuIGl", 3 | "base64_2": "TWF$$$uIGl=", 4 | "base64_3": false, 5 | "extra_field": "aaaa", 6 | 7 | "value_is_hash": {"test": 1}, 8 | "value_is_empty_hash": {}, 9 | "value_is_array": ["test", 1], 10 | "value_is_empty_array": [] 11 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/mongo_id/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "NOT_ID", 3 | "mongo_id_2": "NOT_ID", 4 | "mongo_id_3": "NOT_ID", 5 | 6 | "value_is_hash": "FORMAT_ERROR", 7 | "value_is_empty_hash": "FORMAT_ERROR", 8 | "value_is_array": "FORMAT_ERROR", 9 | "value_is_empty_array": "FORMAT_ERROR" 10 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/is/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": "not the same value", 3 | "is_2": "", 4 | "is_3": 1, 5 | "empty_field": "", 6 | "extra_field": "aaaa", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } 13 | -------------------------------------------------------------------------------- /tests/test_suite/negative/is/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": {"is": "test"}, 3 | "is_2": {"is": [ 1 ]}, 4 | "is_3": [ {"is": true} ], 5 | "empty_field": "is", 6 | 7 | "value_is_hash": "is", 8 | "value_is_empty_hash": "is", 9 | "value_is_array": "is", 10 | "value_is_empty_array": "is" 11 | } 12 | -------------------------------------------------------------------------------- /tests/test_suite/negative/md5/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "NOT_MD5", 3 | "md5_2": "NOT_MD5", 4 | "md5_3": "NOT_MD5", 5 | "md5_4": "NOT_MD5", 6 | 7 | "value_is_hash": "FORMAT_ERROR", 8 | "value_is_empty_hash": "FORMAT_ERROR", 9 | "value_is_array": "FORMAT_ERROR", 10 | "value_is_empty_array": "FORMAT_ERROR" 11 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/mongo_id/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "mongo_id", 3 | "mongo_id_2": { "mongo_id": [] }, 4 | "mongo_id_3": [ { "mongo_id": [] } ], 5 | 6 | "value_is_hash": "mongo_id", 7 | "value_is_empty_hash": "mongo_id", 8 | "value_is_array": "mongo_id", 9 | "value_is_empty_array": "mongo_id" 10 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/required_if/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "sendMeEmails": { "one_of": [0, 1] }, 3 | "users": "required", 4 | "zipCode": "required", 5 | 6 | "required_if_1": {"required_if": {"sendMeEmails": 1 } }, 7 | "required_if_2": {"required_if": {"users/3/address/city": "Kyiv"}}, 8 | "required_if_3": {"required_if": {"zipCode": "01246"}} 9 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/base64/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "base64_1": "MALFORMED_BASE64", 3 | "base64_2": "MALFORMED_BASE64", 4 | "base64_3": "MALFORMED_BASE64", 5 | 6 | "value_is_hash": "FORMAT_ERROR", 7 | "value_is_empty_hash": "FORMAT_ERROR", 8 | "value_is_array": "FORMAT_ERROR", 9 | "value_is_empty_array": "FORMAT_ERROR" 10 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/boolean/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean_1": "NOT_BOOLEAN", 3 | "boolean_2": "NOT_BOOLEAN", 4 | "boolean_3": "NOT_BOOLEAN", 5 | 6 | "value_is_hash": "FORMAT_ERROR", 7 | "value_is_empty_hash": "FORMAT_ERROR", 8 | "value_is_array": "FORMAT_ERROR", 9 | "value_is_empty_array": "FORMAT_ERROR" 10 | } 11 | -------------------------------------------------------------------------------- /tests/test_suite/negative/instance_of/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "dog1": "WRONG_INSTANCE", 3 | "dog2": "WRONG_INSTANCE", 4 | 5 | "value_is_string": "FORMAT_ERROR", 6 | "value_is_hash": "WRONG_INSTANCE", 7 | "value_is_empty_hash": "WRONG_INSTANCE", 8 | "value_is_array": "WRONG_INSTANCE", 9 | "value_is_empty_array": "WRONG_INSTANCE" 10 | } 11 | -------------------------------------------------------------------------------- /tests/test_suite/negative/ipv4/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "NOT_IP", 3 | "ipv4_2": "NOT_IP", 4 | "ipv4_3": "NOT_IP", 5 | "ipv4_4": "NOT_IP", 6 | "ipv4_5": "NOT_IP", 7 | 8 | "value_is_hash": "FORMAT_ERROR", 9 | "value_is_empty_hash": "FORMAT_ERROR", 10 | "value_is_array": "FORMAT_ERROR", 11 | "value_is_empty_array": "FORMAT_ERROR" 12 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/uuid/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": { "uuid": "v1" }, 3 | "uuid_2": { "uuid": "v3" }, 4 | "uuid_3": { "uuid": "v4" }, 5 | "uuid_4": { "uuid": "v5" }, 6 | "uuid_5": "uuid", 7 | 8 | "value_is_hash": "uuid", 9 | "value_is_empty_hash": "uuid", 10 | "value_is_array": "uuid", 11 | "value_is_empty_array": "uuid" 12 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/required_if/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "sendMeEmails": 1, 3 | "address": { 4 | "city": "Kyiv", 5 | "street": "Smolna" 6 | }, 7 | "zipCode": "01246", 8 | 9 | "required_if_1": "", 10 | "required_if_2": [], 11 | "required_if_3": "", 12 | "required_if_4": "", 13 | "required_if_5": "", 14 | 15 | "empty_field": "" 16 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/ipv4/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "ipv4", 3 | "ipv4_2": { "ipv4": [] }, 4 | "ipv4_3": [ { "ipv4": [] } ], 5 | "ipv4_4": "ipv4", 6 | "ipv4_5": "ipv4", 7 | "empty_field": "ipv4", 8 | 9 | "value_is_hash": "ipv4", 10 | "value_is_empty_hash": "ipv4", 11 | "value_is_array": "ipv4", 12 | "value_is_empty_array": "ipv4" 13 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/mongo_id/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_id_1": "5a5622616acdc6684a892", 3 | "mongo_id_2": "zzzzzzzzzzzzzzzzzzzzzzzz", 4 | "mongo_id_3": "0x3333333333333333333333", 5 | "extra_field": "aaaa", 6 | 7 | "value_is_hash": {"test": 1}, 8 | "value_is_empty_hash": {}, 9 | "value_is_array": ["test", 1], 10 | "value_is_empty_array": [] 11 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_items_unique/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", "test", 0, 1, 2], 3 | "list_2": [1, 1, {"test": 1}], 4 | "list_3": [ [], [] ], 5 | "extra_field": "aaaa", 6 | 7 | "value_is_number": 7, 8 | "value_is_string": "test", 9 | "value_is_boolean": false, 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {} 12 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/uuid/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": "NOT_UUID", 3 | "uuid_2": "NOT_UUID", 4 | "uuid_3": "NOT_UUID", 5 | "uuid_4": "NOT_UUID", 6 | "uuid_5": "NOT_UUID", 7 | 8 | "value_is_hash": "FORMAT_ERROR", 9 | "value_is_empty_hash": "FORMAT_ERROR", 10 | "value_is_array": "FORMAT_ERROR", 11 | "value_is_empty_array": "FORMAT_ERROR" 12 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/is/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_1": "NOT_ALLOWED_VALUE", 3 | "is_2": "REQUIRED", 4 | "is_3": "NOT_ALLOWED_VALUE", 5 | "empty_field": "REQUIRED", 6 | 7 | "value_is_hash": "FORMAT_ERROR", 8 | "value_is_empty_hash": "FORMAT_ERROR", 9 | "value_is_array": "FORMAT_ERROR", 10 | "value_is_empty_array": "FORMAT_ERROR" 11 | } 12 | -------------------------------------------------------------------------------- /tests/test_suite/positive/uuid/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 3 | "uuid_2": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 4 | "uuid_3": "b62db7da-f9e9-3476-795e-f3a0f695a6f1", 5 | "uuid_4": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 6 | "uuid_5": "b62db7da-f9e9-5476-a95e-f3a0f695a6f1", 7 | "uuid_6": "b62dB7da-F9e9-4476-a95E-f3a0f695a6f1", 8 | "empty_field": "" 9 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_items_unique/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": "NOT_UNIQUE_ITEMS", 3 | "list_2": "INCOMPARABLE_ITEMS", 4 | "list_3": "INCOMPARABLE_ITEMS", 5 | 6 | "value_is_number": "FORMAT_ERROR", 7 | "value_is_string": "FORMAT_ERROR", 8 | "value_is_boolean": "FORMAT_ERROR", 9 | "value_is_hash": "FORMAT_ERROR", 10 | "value_is_empty_hash": "FORMAT_ERROR" 11 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/required_if/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "sendMeEmails": 1, 3 | "address": { 4 | "city": "Kyiv", 5 | "street": "Smolna" 6 | }, 7 | "zipCode": "01246", 8 | 9 | "required_if_1": "", 10 | "required_if_2": [], 11 | "required_if_3": "", 12 | "required_if_4": "", 13 | "required_if_5": "", 14 | 15 | "empty_field": "", 16 | "extra_field": "aaaa" 17 | } -------------------------------------------------------------------------------- /src/rules/mongo_id.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const objectIdRe = /^[0-9a-fA-F]{24}$/; 3 | 4 | function mongo_id() { 5 | return value => { 6 | if (util.isNoValue(value)) return; 7 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 8 | 9 | if (!objectIdRe.test(value + '')) return 'NOT_ID'; 10 | 11 | return; 12 | }; 13 | } 14 | 15 | module.exports = mongo_id; 16 | -------------------------------------------------------------------------------- /tests/test_suite/negative/instance_of/input.js: -------------------------------------------------------------------------------- 1 | const Cat = require('./samples/Cat'); 2 | 3 | module.exports = { 4 | "dog1": Cat, 5 | "dog2": Cat, 6 | "empty_field": "", 7 | "extra_field": "aaaa", 8 | 9 | "value_is_string": "test", 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_length/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": "TOO_FEW_ITEMS", 3 | "list_2": "TOO_MANY_ITEMS", 4 | "list_3": "TOO_FEW_ITEMS", 5 | "empty_field": "REQUIRED", 6 | 7 | "value_is_number": "FORMAT_ERROR", 8 | "value_is_string": "FORMAT_ERROR", 9 | "value_is_boolean": "FORMAT_ERROR", 10 | "value_is_hash": "FORMAT_ERROR", 11 | "value_is_empty_hash": "FORMAT_ERROR" 12 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": ["test", 1, {}, 2, [], 3, true], 3 | "list_2": ["test", 1, {}, 2, [], 3, true], 4 | "list_3": [], 5 | "empty_field": "", 6 | "extra_field": "aaaa", 7 | 8 | "value_is_number": 7, 9 | "value_is_string": "test", 10 | "value_is_boolean": false, 11 | "value_is_hash": {"test": 1}, 12 | "value_is_empty_hash": {} 13 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_items_unique/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": "list_items_unique", 3 | "list_2": "list_items_unique", 4 | "list_3": "list_items_unique", 5 | 6 | "value_is_number": "list_items_unique", 7 | "value_is_string": "list_items_unique", 8 | "value_is_boolean": "list_items_unique", 9 | "value_is_hash": "list_items_unique", 10 | "value_is_empty_hash": "list_items_unique" 11 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/ipv4/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_1": "localhost", 3 | "ipv4_2": "192.257.22.1", 4 | "ipv4_3": "255.255.05.255", 5 | "ipv4_4": "192.168.1.1.2", 6 | "ipv4_5": "192.168.1.2a2", 7 | "empty_field": "", 8 | "extra_field": "aaaa", 9 | 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/has_methods/input.js: -------------------------------------------------------------------------------- 1 | const Cat = require('./samples/Cat'); 2 | 3 | module.exports = { 4 | "dog1": Cat, 5 | "dog2": Cat, 6 | "dog3": Cat, 7 | "empty_field": "", 8 | "extra_field": "aaaa", 9 | 10 | "value_is_string": "test", 11 | "value_is_hash": {"test": 1}, 12 | "value_is_empty_hash": {}, 13 | "value_is_array": ["test", 1], 14 | "value_is_empty_array": [] 15 | }; -------------------------------------------------------------------------------- /tests/test_suite/negative/required_if/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "sendMeEmails": 1, 3 | "users" : [ 4 | {}, {}, {}, 5 | { 6 | "address": { 7 | "city": "Kyiv", 8 | "street": "Smolna" 9 | } 10 | } 11 | ], 12 | "zipCode": "01246", 13 | 14 | "required_if_1": "", 15 | "required_if_2": "", 16 | "required_if_3": "", 17 | 18 | "extra_field": "aaaa" 19 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/uuid/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 3 | "uuid_2": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 4 | "uuid_3": "b62db7da-f9e9-3476-795e-f3a0f695a6f1", 5 | "uuid_4": "b62db7da-f9e9-4476-a95e-f3a0f695a6f1", 6 | "uuid_5": "b62db7da-f9e9-5476-a95e-f3a0f695a6f1", 7 | "uuid_6": "b62dB7da-F9e9-4476-a95E-f3a0f695a6f1", 8 | "empty_field": "", 9 | "extra_field": "aaaa" 10 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/md5/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "md5_1": "1BC29B36F623BA82AAF6724FD3", 3 | "md5_2": "d41d8cd98f00b204e9800998ecf8427e84815", 4 | "md5_3": "ZZZ12345678901234567890123456789", 5 | "md5_4": 12345678901234567890123456789012, 6 | "extra_field": "aaaa", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/iso_date/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1": "2018-01-10", 3 | "date_2": "2018-01-10T00:00:00.000Z", 4 | "date_3": "2018-01-10T15:07:00.000Z", 5 | "date_4": "2018-01-10T15:07:04.000Z", 6 | "date_5": "2018-01-10T15:07:04.609Z", 7 | "date_6": "2018-01-10T15:07:00.000Z", 8 | "date_7": "2018-01-10T10:07:04.000Z", 9 | "date_8": "2018-01-10T19:07:04.609Z", 10 | "date_9": "2018-01-09", 11 | "empty_field": "" 12 | } -------------------------------------------------------------------------------- /src/rules/md5.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const md5Re = /^[a-f0-9]{32}$/i; 3 | 4 | function md5() { 5 | return (value, params, outputArr) => { 6 | if (util.isNoValue(value)) return; 7 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 8 | 9 | if (!md5Re.test(value + '')) return 'NOT_MD5'; 10 | 11 | outputArr.push(value + ''); 12 | 13 | return; 14 | }; 15 | } 16 | 17 | module.exports = md5; 18 | -------------------------------------------------------------------------------- /tests/test_suite/negative/credit_card/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "98989898989898", 3 | "credit_card_2": "9898989898989898989898989898989898", 4 | "credit_card_3": "11", 5 | "credit_card_4": true, 6 | "credit_card_5": 98989898989898, 7 | "credit_card_6": "98989a98989899", 8 | 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } 14 | -------------------------------------------------------------------------------- /tests/test_suite/negative/has_methods/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "dog1": "NOT_HAVING_METHOD [bark]", 3 | "dog2": "NOT_HAVING_METHOD [bark]", 4 | "dog3": "NOT_HAVING_METHOD [jump]", 5 | 6 | "value_is_string": "FORMAT_ERROR", 7 | "value_is_hash": "NOT_HAVING_METHOD [bark]", 8 | "value_is_empty_hash": "NOT_HAVING_METHOD [bark]", 9 | "value_is_array": "NOT_HAVING_METHOD [bark]", 10 | "value_is_empty_array": "NOT_HAVING_METHOD [bark]" 11 | } 12 | -------------------------------------------------------------------------------- /tests/test_suite/negative/credit_card/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "credit_card", 3 | "credit_card_2": "credit_card", 4 | "credit_card_3": "credit_card", 5 | "credit_card_4": "credit_card", 6 | "credit_card_5": "credit_card", 7 | "credit_card_6": "credit_card", 8 | 9 | "value_is_hash": "credit_card", 10 | "value_is_empty_hash": "credit_card", 11 | "value_is_array": "credit_card", 12 | "value_is_empty_array": "credit_card" 13 | } 14 | -------------------------------------------------------------------------------- /tests/test_suite/negative/uuid/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid_1": "test", 3 | "uuid_2": "b62db7da-f9e9-3476-a95e-", 4 | "uuid_3": "b62db7da-f9e9-3476-795e-f3a0f695a6f1", 5 | "uuid_4": "GGGdb7da-f9e9-4476-a95e-f3a0f695a6f1", 6 | "uuid_5": "b62db7da-f9e9-5476-a95e-f3a0f695a6f1", 7 | "extra_field": "aaaa", 8 | 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } -------------------------------------------------------------------------------- /tests/test_suite/positive/iso_date/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1": "2018-01-10T15:07:04.609Z", 3 | "date_2": "2018-01-10", 4 | "date_3": "2018-01-10T15:07Z", 5 | "date_4": "2018-01-10T15:07:04Z", 6 | "date_5": "2018-01-10T15:07:04.609Z", 7 | "date_6": "2018-01-10T15:07Z", 8 | "date_7": "2018-01-10T15:07:04+05:00", 9 | "date_8": "2018-01-10T15:07:04.609-04:00", 10 | "date_9": "2018-01-10T02:00+05:00", 11 | "empty_field": "", 12 | "extra_field": "aaaa" 13 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/list_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_1": {"list_length": 10}, 3 | "list_2": {"list_length": 3}, 4 | "list_3": {"list_length": [3, 5]}, 5 | "empty_field": ["required", { "list_length": 5 }], 6 | 7 | "value_is_number": {"list_length": 10}, 8 | "value_is_string": {"list_length": 10}, 9 | "value_is_boolean": {"list_length": 10}, 10 | "value_is_hash": {"list_length": 10}, 11 | "value_is_empty_hash": {"list_length": 10} 12 | } -------------------------------------------------------------------------------- /src/rules/is.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function is(allowedValue) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return 'REQUIRED'; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | if (value + '' === allowedValue + '') { 9 | outputArr.push(allowedValue); 10 | return; 11 | } 12 | 13 | return 'NOT_ALLOWED_VALUE'; 14 | }; 15 | } 16 | 17 | module.exports = is; 18 | -------------------------------------------------------------------------------- /tests/test_suite/negative/instance_of/rules.js: -------------------------------------------------------------------------------- 1 | const Dog = require('./samples/Dog'); 2 | 3 | module.exports = { 4 | "dog1": {"instance_of": Dog}, 5 | "dog2": {"instance_of": [Dog]}, 6 | "empty_field": {"instance_of": Dog}, 7 | 8 | "value_is_string": {"instance_of": Dog}, 9 | "value_is_hash": {"instance_of": Dog}, 10 | "value_is_empty_hash": {"instance_of": Dog}, 11 | "value_is_array": {"instance_of": Dog}, 12 | "value_is_empty_array": {"instance_of": Dog} 13 | }; -------------------------------------------------------------------------------- /tests/test_suite/negative/has_methods/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "dog1": {"has_methods": "bark"}, 3 | "dog2": {"has_methods": ["bark", "jump"]}, 4 | "dog3": {"has_methods": ["jump", "bark"]}, 5 | "empty_field": {"has_methods": "bark"}, 6 | 7 | "value_is_string": {"has_methods": "bark"}, 8 | "value_is_hash": {"has_methods": "bark"}, 9 | "value_is_empty_hash": {"has_methods": "bark"}, 10 | "value_is_array": {"has_methods": "bark"}, 11 | "value_is_empty_array": {"has_methods": "bark"} 12 | } -------------------------------------------------------------------------------- /src/rules/instance_of.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function instance_of(expectedClass) { 4 | if (!expectedClass) { 5 | throw new Error('LIVR: instance_of requires class'); 6 | } 7 | 8 | return (value, params, outputArr) => { 9 | if (util.isNoValue(value)) return; 10 | if (util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 11 | 12 | if (!(value instanceof expectedClass)) return 'WRONG_INSTANCE'; 13 | return; 14 | }; 15 | } 16 | 17 | module.exports = instance_of; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "noEmit": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "livr/types/inference": ["../js-validator-livr/types/inference"], 13 | "livr/types": ["../js-validator-livr/types"] 14 | } 15 | }, 16 | "include": ["test-types.ts", "types/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /tests/test_suite/negative/credit_card/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_1": "WRONG_CREDIT_CARD_NUMBER", 3 | "credit_card_2": "WRONG_CREDIT_CARD_NUMBER", 4 | "credit_card_3": "WRONG_CREDIT_CARD_NUMBER", 5 | "credit_card_4": "WRONG_CREDIT_CARD_NUMBER", 6 | "credit_card_5": "WRONG_CREDIT_CARD_NUMBER", 7 | "credit_card_6": "WRONG_CREDIT_CARD_NUMBER", 8 | 9 | "value_is_hash": "FORMAT_ERROR", 10 | "value_is_empty_hash": "FORMAT_ERROR", 11 | "value_is_array": "FORMAT_ERROR", 12 | "value_is_empty_array": "FORMAT_ERROR" 13 | } 14 | -------------------------------------------------------------------------------- /tests/test_suite/positive/required_if/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "sendMeEmails": { "one_of": [0, 1] }, 3 | "address": {"nested_object": { 4 | "city": "required", 5 | "street": "required" 6 | }}, 7 | "zipCode": "required", 8 | 9 | "required_if_1": {"required_if": {"sendMeEmails": 2}}, 10 | "required_if_2": {"required_if": {"address/city": "Kyiv"}}, 11 | "required_if_3": {"required_if": {"zipCode": "98754"}}, 12 | "required_if_4": {"required_if": {"noSuchField": "something"}}, 13 | "required_if_5": {"required_if": {"users/3/address/city": "Kyiv"}}, 14 | 15 | "empty_field": "required_if" 16 | } -------------------------------------------------------------------------------- /src/rules/boolean.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function boolean() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | if (value === true || value === 1 || value === 'true' || value === '1') { 9 | outputArr.push(true); 10 | return; 11 | } 12 | if (value === false || value === 0 || value === 'false' || value === '0') { 13 | outputArr.push(false); 14 | return; 15 | } 16 | 17 | return 'NOT_BOOLEAN'; 18 | }; 19 | } 20 | 21 | module.exports = boolean; 22 | -------------------------------------------------------------------------------- /src/rules/base64.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const requireRe = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/; 3 | const optionalRe = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/; 4 | 5 | function base64(padding) { 6 | const base64Re = padding === 'relaxed' ? optionalRe : requireRe; 7 | 8 | return (value, params, outputArr) => { 9 | if (util.isNoValue(value)) return; 10 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 11 | 12 | if (!base64Re.test(value + '')) return 'MALFORMED_BASE64'; 13 | 14 | outputArr.push(value + ''); 15 | 16 | return; 17 | }; 18 | } 19 | 20 | module.exports = base64; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'base64': require('./rules/base64'), 3 | 'boolean': require('./rules/boolean'), 4 | 'credit_card': require('./rules/credit_card'), 5 | 'ipv4': require('./rules/ipv4'), 6 | 'is': require('./rules/is'), 7 | 'iso_date': require('./rules/iso_date'), 8 | 'list_items_unique': require('./rules/list_items_unique'), 9 | 'list_length': require('./rules/list_length'), 10 | 'md5': require('./rules/md5'), 11 | 'mongo_id': require('./rules/mongo_id'), 12 | 'required_if': require('./rules/required_if'), 13 | 'uuid': require('./rules/uuid'), 14 | 'instance_of': require('./rules/instance_of'), 15 | 'has_methods': require('./rules/has_methods'), 16 | }; 17 | -------------------------------------------------------------------------------- /tests/test_suite/positive/iso_date/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1": "iso_date", 3 | "date_2": {"iso_date": {"min": "2017-10-15T15:30Z", "max": "yesterday", "format": "datetime"} }, 4 | "date_3": {"iso_date": {"min": "2018-01-10T15:07Z", "max": "2018-01-10T15:07Z","format": "datetime"} }, 5 | "date_4": {"iso_date": {"max": "tomorrow", "format": "datetime"} }, 6 | "date_5": {"iso_date": {"min": "2017-12-01T16:00:00.487+05:00", "format": "datetime"} }, 7 | "date_6": {"iso_date": {"min": "2018-01-10", "max": "2018-01-10","format": "datetime"} }, 8 | "date_7": {"iso_date": {"format": "datetime"} }, 9 | "date_8": {"iso_date": {"format": "datetime"} }, 10 | "date_9": {"iso_date": {"format": "date"} }, 11 | "empty_field": "iso_date" 12 | } -------------------------------------------------------------------------------- /src/rules/ipv4.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const ipRe = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 3 | 4 | function ipv4() { 5 | return value => { 6 | if (util.isNoValue(value)) return; 7 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 8 | 9 | const match = (value + '').match(ipRe); 10 | if (!match) return 'NOT_IP'; 11 | 12 | for (let i = 1; i <= 4; i++) { 13 | if (match[i].length >= 2 && match[i][0] === '0') { 14 | return 'NOT_IP'; 15 | } 16 | } 17 | 18 | return; 19 | }; 20 | } 21 | 22 | module.exports = ipv4; 23 | -------------------------------------------------------------------------------- /src/rules/list_items_unique.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function list_items_unique() { 4 | return list => { 5 | if (util.isNoValue(list)) return; 6 | if (!Array.isArray(list)) return 'FORMAT_ERROR'; 7 | 8 | const seen = new Set(); 9 | let hasDuplicate = false; 10 | 11 | for (const item of list) { 12 | if (!util.isPrimitiveValue(item)) return 'INCOMPARABLE_ITEMS'; 13 | if (seen.has(item)) { 14 | hasDuplicate = true; 15 | } else { 16 | seen.add(item); 17 | } 18 | } 19 | 20 | if (hasDuplicate) return 'NOT_UNIQUE_ITEMS'; 21 | 22 | return; 23 | }; 24 | } 25 | 26 | module.exports = list_items_unique; 27 | -------------------------------------------------------------------------------- /src/rules/has_methods.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function has_methods(requiredMethods) { 4 | if (!Array.isArray(requiredMethods)) { 5 | requiredMethods = Array.prototype.slice.call(arguments); 6 | requiredMethods.pop(); // pop ruleBuilders 7 | } 8 | 9 | return (value, params, outputArr) => { 10 | if (util.isNoValue(value)) return; 11 | if (util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 12 | 13 | for (const method of requiredMethods) { 14 | if (!value[method] || typeof value[method] !== 'function' ) { 15 | return `NOT_HAVING_METHOD [${method}]` 16 | } 17 | } 18 | 19 | return; 20 | }; 21 | } 22 | 23 | module.exports = has_methods; 24 | -------------------------------------------------------------------------------- /src/rules/list_length.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function list_length(param1, param2) { 4 | let minLen; 5 | let maxLen; 6 | 7 | if (arguments.length <= 1) { 8 | throw new Error('LIVR: undefined list_length'); 9 | } else if (arguments.length === 2) { 10 | minLen = param1; 11 | maxLen = param1; 12 | } else if (arguments.length > 2) { 13 | minLen = param1; 14 | maxLen = param2; 15 | } 16 | 17 | return list => { 18 | if (util.isNoValue(list)) return; 19 | if (!Array.isArray(list)) return 'FORMAT_ERROR'; 20 | 21 | if (list.length < minLen) return 'TOO_FEW_ITEMS'; 22 | if (list.length > maxLen) return 'TOO_MANY_ITEMS'; 23 | 24 | return; 25 | }; 26 | } 27 | 28 | module.exports = list_length; 29 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isPrimitiveValue(value) { 3 | if (typeof value == 'string') return true; 4 | if (typeof value == 'number' && isFinite(value)) return true; 5 | if (typeof value == 'boolean') return true; 6 | return false; 7 | }, 8 | 9 | // looksLikeNumber(value) { 10 | // if (!isNaN(+value)) return true; 11 | // return false; 12 | // }, 13 | 14 | isNoValue(value) { 15 | return value === undefined || value === null || value === ''; 16 | }, 17 | 18 | JSONPointer(object, pointer) { 19 | const parts = pointer.split('/'); 20 | let value = object; 21 | 22 | for (const part of parts) { 23 | if (!value) break; 24 | value = value[part]; 25 | } 26 | 27 | return value; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.6.0] - 2025-12-03 6 | 7 | ### Changed 8 | 9 | - **Performance improvements (~18% faster overall)** 10 | - Replace `String.match()` with `RegExp.test()` for pattern validation in md5, mongo_id, uuid, base64, and credit_card rules 11 | - Replace `Array.indexOf()` with direct comparison in boolean rule 12 | - Use `Set` instead of object for list_items_unique uniqueness check 13 | - Replace regex check with string indexing for ipv4 leading zero validation 14 | - Use `slice()` instead of `split()` for iso_date output formatting 15 | 16 | ### Fixed 17 | 18 | - ipv4 rule now correctly checks all 4 octets for leading zeros (previously only checked first 3) 19 | 20 | ## [1.5.2] - Previous releases 21 | 22 | - See git history for previous changes 23 | -------------------------------------------------------------------------------- /src/rules/required_if.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | function required_if(query) { 4 | let queryKey; 5 | let queryValue; 6 | 7 | if (arguments.length > 1) { 8 | queryKey = Object.keys(query)[0]; 9 | queryValue = query[queryKey]; 10 | 11 | if (!queryValue || !util.isPrimitiveValue(queryValue)) { 12 | throw new Error( 13 | 'LIVR: the target value of the "require_if" rule is missed or incomparable' 14 | ); 15 | } 16 | } 17 | 18 | return (value, params) => { 19 | if (!util.isNoValue(value) || !queryKey) return; 20 | 21 | var valueToCheck = util.JSONPointer(params, queryKey); 22 | 23 | if (valueToCheck == queryValue && util.isNoValue(value)) return 'REQUIRED'; 24 | 25 | return; 26 | }; 27 | } 28 | 29 | module.exports = required_if; 30 | -------------------------------------------------------------------------------- /src/rules/credit_card.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const numRe = /^\d*$/; 3 | 4 | function credit_card() { 5 | return (value, params, outputArr) => { 6 | if (util.isNoValue(value)) return; 7 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 8 | 9 | value = value + ''; 10 | 11 | if (value.length > 16 || value.length < 14) return 'WRONG_CREDIT_CARD_NUMBER'; 12 | 13 | if (!numRe.test(value)) return 'WRONG_CREDIT_CARD_NUMBER'; 14 | 15 | let n = value.length; 16 | let sum = 0; 17 | let p = false; 18 | 19 | while (n--) { 20 | var digit = value.charAt(n) * (1 + p); 21 | 22 | sum += digit - (digit > 9) * 9; 23 | p = !p; 24 | } 25 | 26 | if (sum % 10) return 'WRONG_CREDIT_CARD_NUMBER'; 27 | 28 | return; 29 | }; 30 | } 31 | 32 | module.exports = credit_card; 33 | -------------------------------------------------------------------------------- /src/rules/uuid.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const uuidRe = { 3 | v1: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, 4 | v2: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, 5 | v3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i, 6 | v4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, 7 | v5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i 8 | }; 9 | 10 | function uuid(version) { 11 | if (arguments.length == 1) { 12 | version = 'v4'; 13 | } 14 | 15 | if (!['v1', 'v2', 'v3', 'v4', 'v5'].includes(version)) { 16 | throw new Error('LIVR: unsupported uuid version: ' + version); 17 | } 18 | 19 | return value => { 20 | if (util.isNoValue(value)) return; 21 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 22 | 23 | if (!uuidRe[version].test(value + '')) return 'NOT_UUID'; 24 | return; 25 | }; 26 | } 27 | 28 | module.exports = uuid; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Viktor Turskyi 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. -------------------------------------------------------------------------------- /tests/test_suite/negative/iso_date/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1": "DATE_TOO_LOW", 3 | "date_2": "DATE_TOO_LOW", 4 | "date_3": "DATE_TOO_LOW", 5 | "date_4": "DATE_TOO_LOW", 6 | "date_5": "DATE_TOO_LOW", 7 | "date_6": "DATE_TOO_LOW", 8 | "date_7": "DATE_TOO_LOW", 9 | "date_8": "DATE_TOO_LOW", 10 | 11 | "date_9": "DATE_TOO_HIGH", 12 | "date_10": "DATE_TOO_HIGH", 13 | "date_11": "DATE_TOO_HIGH", 14 | "date_12": "DATE_TOO_HIGH", 15 | "date_13": "DATE_TOO_HIGH", 16 | "date_14": "DATE_TOO_HIGH", 17 | "date_15": "DATE_TOO_HIGH", 18 | "date_16": "DATE_TOO_HIGH", 19 | 20 | "date_17": "WRONG_DATE", 21 | "date_18": "WRONG_DATE", 22 | "date_19": "WRONG_DATE", 23 | "date_20": "WRONG_DATE", 24 | "date_21": "WRONG_DATE", 25 | "date_22": "WRONG_DATE", 26 | "date_23": "WRONG_DATE", 27 | "date_24": "WRONG_DATE", 28 | "date_25": "WRONG_DATE", 29 | "date_26": "WRONG_DATE", 30 | 31 | "value_is_hash": "FORMAT_ERROR", 32 | "value_is_empty_hash": "FORMAT_ERROR", 33 | "value_is_array": "FORMAT_ERROR", 34 | "value_is_empty_array": "FORMAT_ERROR" 35 | } -------------------------------------------------------------------------------- /tests/test_suite/negative/iso_date/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1": "2018-01-10", 3 | "date_2": "2018-01-10T03:14:26.464+05:00", 4 | "date_3": "2018-01-10T03:14:25.465+05:00", 5 | "date_4": "2018-01-10T03:13:26.465+05:00", 6 | "date_5": "2018-01-10T00:00+01:00", 7 | "date_6": "2018-12-01", 8 | "date_7": "2018-11-02", 9 | "date_8": "2017-12-02", 10 | 11 | "date_9": "2018-01-10T23:45:00Z", 12 | "date_10": "2018-01-10T03:14:26.466-05:00", 13 | "date_11": "2018-01-10T03:14:27.465-05:00", 14 | "date_12": "2018-01-10T03:15:26.465-05:00", 15 | "date_13": "2018-01-10T23:00-01:00", 16 | "date_14": "2018-11-03", 17 | "date_15": "2018-12-02", 18 | "date_16": "2019-11-02", 19 | 20 | "date_17": " 2018-01-31", 21 | "date_18": "2oi8-oi-3i", 22 | "date_19": "201й-01-31", 23 | "date_20": "2015-02-29", 24 | "date_21": "2018-03-31T15:35", 25 | "date_22": "2018-04-30t16:.000Z", 26 | "date_23": "2018-05-31Z", 27 | "date_24": "2018-06-30&23:59Z", 28 | "date_25": "2018-07-31T15:000Z", 29 | "date_26": "2018-07-31T15:75Z", 30 | 31 | "value_is_hash": {"test": 1}, 32 | "value_is_empty_hash": {}, 33 | "value_is_array": ["test", 1], 34 | "value_is_empty_array": [] 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livr-extra-rules", 3 | "version": "1.6.0", 4 | "description": "Extra rules for LIVR Validator", 5 | "main": "src/index.js", 6 | "types": "types/index.d.ts", 7 | "directories": { 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "test": "ava && tsc --noEmit", 12 | "coverage": "nyc ava" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/koorchik/js-livr-extra-rules.git" 17 | }, 18 | "keywords": [ 19 | "Validation", 20 | "LIVR", 21 | "Schema", 22 | "Validator", 23 | "Rules" 24 | ], 25 | "peerDependencies": { 26 | "livr": "*" 27 | }, 28 | "author": "koorchik", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/koorchik/js-livr-extra-rules/issues" 32 | }, 33 | "homepage": "https://github.com/koorchik/js-livr-extra-rules#readme", 34 | "devDependencies": { 35 | "ava": "^6.4.1", 36 | "livr": "^2.9.0", 37 | "typescript": "^5.9.3" 38 | }, 39 | "ava": { 40 | "files": [ 41 | "tests/*.js" 42 | ] 43 | }, 44 | "nyc": { 45 | "check-coverage": true, 46 | "per-file": true, 47 | "lines": 85, 48 | "statements": 85, 49 | "functions": 85, 50 | "branches": 85, 51 | "exclude": [ 52 | "tests/*.js" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/test_suite/negative/iso_date/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "date_1" : {"iso_date": {"min": "current"} }, 3 | "date_2" : {"iso_date": {"min": "2018-01-10T03:14:26.465+05:00"} }, 4 | "date_3" : {"iso_date": {"min": "2018-01-10T03:14:26.465+05:00"} }, 5 | "date_4" : {"iso_date": {"min": "2018-01-10T03:14:26.465+05:00"} }, 6 | "date_5" : {"iso_date": {"min": "2018-01-10"} }, 7 | "date_6" : {"iso_date": {"min": "2018-12-02"} }, 8 | "date_7" : {"iso_date": {"min": "2018-12-02"} }, 9 | "date_8" : {"iso_date": {"min": "2018-12-02"} }, 10 | 11 | "date_9" : {"iso_date": {"max": "2018-01-10T21:00-01:00"} }, 12 | "date_10" : {"iso_date": {"max": "2018-01-10T03:14:26.465-05:00"} }, 13 | "date_11" : {"iso_date": {"max": "2018-01-10T03:14:26.465-05:00"} }, 14 | "date_12" : {"iso_date": {"max": "2018-01-10T03:14:26.465-05:00"} }, 15 | "date_13" : {"iso_date": {"max": "2018-01-10"} }, 16 | "date_14" : {"iso_date": {"max": "2018-11-02"} }, 17 | "date_15" : {"iso_date": {"max": "2018-11-02"} }, 18 | "date_16" : {"iso_date": {"max": "2018-11-02"} }, 19 | 20 | "date_17" : "iso_date", 21 | "date_18" : "iso_date", 22 | "date_19" : "iso_date", 23 | "date_20" : "iso_date", 24 | "date_21" : "iso_date", 25 | "date_22" : "iso_date", 26 | "date_23" : "iso_date", 27 | "date_24" : "iso_date", 28 | "date_25" : "iso_date", 29 | "date_26" : "iso_date", 30 | 31 | "value_is_hash": "iso_date", 32 | "value_is_empty_hash": "iso_date", 33 | "value_is_array": "iso_date", 34 | "value_is_empty_array": "iso_date" 35 | } -------------------------------------------------------------------------------- /tests/test_suite.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const LIVR = require('livr'); 3 | const util = require('util'); 4 | const test = require('ava'); 5 | 6 | const extraRules = require('../src'); 7 | 8 | LIVR.Validator.registerDefaultRules(extraRules); 9 | 10 | iterateTestData('test_suite/positive', function(data) { 11 | test(`LIVR positive tests: ${data.name}`, t => { 12 | const validator = new LIVR.Validator(data.rules); 13 | const output = validator.validate(data.input); 14 | 15 | const errors = validator.getErrors(); 16 | 17 | t.true( 18 | !errors, 19 | 'Validator should contain no errors. The error was ' + util.inspect(errors) 20 | ); 21 | 22 | t.deepEqual(output, data.output, 'Output should contain correct data'); 23 | }); 24 | }); 25 | 26 | iterateTestData('test_suite/negative', function(data) { 27 | test(`LIVR negative tests: ${data.name}`, t => { 28 | const validator = new LIVR.Validator(data.rules); 29 | const output = validator.validate(data.input); 30 | 31 | t.true(!output, 'Output should be false'); 32 | t.deepEqual(validator.getErrors(), data.errors, 'Validator should contain errors'); 33 | }); 34 | }); 35 | 36 | function iterateTestData(path, cb) { 37 | const rootPath = __dirname + '/' + path; 38 | console.log(`ITERATE: ${rootPath}`); 39 | const casesDirs = fs.readdirSync(rootPath); 40 | 41 | for (const caseDir of casesDirs) { 42 | const caseFiles = fs.readdirSync(rootPath + '/' + caseDir); 43 | const caseData = { name: caseDir }; 44 | 45 | for (const file of caseFiles) { 46 | const fullName = rootPath + '/' + caseDir + '/' + file; 47 | let data = {}; 48 | if ( fullName.match(/\.json$/) ) { 49 | const json = fs.readFileSync(fullName); 50 | data = JSON.parse(json); 51 | } else if ( fullName.match(/\.js$/) ) { 52 | data = require(fullName); 53 | } 54 | 55 | caseData[file.replace(/\.js(on)?$/, '')] = data; 56 | } 57 | 58 | cb(caseData); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | livr-extra-rules is an extension library for [LIVR (Language Independent Validation Rules)](http://livr-spec.org/) that provides additional validation rules beyond the core specification. It has **zero dependencies** and works with the JavaScript LIVR implementation. 8 | 9 | ## Commands 10 | 11 | - **Run all tests**: `npm test` 12 | - **Run tests with coverage**: `npm run coverage` 13 | - **Run a single test**: `npx ava tests/test_suite.js --match "LIVR positive tests: rule_name"` or `--match "LIVR negative tests: rule_name"` 14 | 15 | ## Architecture 16 | 17 | ### Rule Structure 18 | 19 | Each validation rule is a factory function in `src/rules/` that returns a validator function. The validator function: 20 | - Receives the value to validate 21 | - Returns `undefined` if valid 22 | - Returns an error code string (e.g., `'NOT_IP'`, `'FORMAT_ERROR'`) if invalid 23 | 24 | Example pattern from `src/rules/ipv4.js`: 25 | ```js 26 | function ipv4() { 27 | return value => { 28 | if (util.isNoValue(value)) return; 29 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 30 | // validation logic... 31 | return; // valid 32 | }; 33 | } 34 | ``` 35 | 36 | ### Utility Functions (`src/util.js`) 37 | 38 | - `isPrimitiveValue(value)` - checks if value is string, finite number, or boolean 39 | - `isNoValue(value)` - checks if value is undefined, null, or empty string 40 | - `JSONPointer(object, pointer)` - traverses object using JSON pointer notation (e.g., `'address/city'`) 41 | 42 | ### Test Structure 43 | 44 | Tests use AVA and are organized in `tests/test_suite/`: 45 | - `positive/` - tests where validation should pass 46 | - `negative/` - tests where validation should fail with specific errors 47 | 48 | Each rule has its own directory containing: 49 | - `rules.json` - LIVR validation schema 50 | - `input.json` (or `.js`) - test input data 51 | - `output.json` (or `.js`) - expected output (positive tests) 52 | - `errors.json` - expected error codes (negative tests) 53 | 54 | ### Adding a New Rule 55 | 56 | 1. Create rule file in `src/rules/your_rule.js` 57 | 2. Export from `src/index.js` 58 | 3. Add positive tests in `tests/test_suite/positive/your_rule/` 59 | 4. Add negative tests in `tests/test_suite/negative/your_rule/` 60 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { RuleTypeDef, ParameterizedRuleDef } from 'livr/types/inference'; 2 | 3 | // ============================================================================ 4 | // Type Inference for livr-extra-rules 5 | // ============================================================================ 6 | 7 | // Module augmentation for simple rules (fixed output types) 8 | declare module 'livr/types/inference' { 9 | // Add instance_of template for instanceOf rule 10 | interface TemplateOutputRegistry { 11 | instance_of: Args extends abstract new (...args: any) => any ? InstanceType : unknown; 12 | } 13 | 14 | interface RuleTypeRegistry { 15 | // String validators 16 | base64: RuleTypeDef; 17 | credit_card: RuleTypeDef; 18 | creditCard: RuleTypeRegistry['credit_card']; 19 | ipv4: RuleTypeDef; 20 | md5: RuleTypeDef; 21 | mongo_id: RuleTypeDef; 22 | mongoId: RuleTypeRegistry['mongo_id']; 23 | uuid: RuleTypeDef; 24 | iso_date: RuleTypeDef; 25 | isoDate: RuleTypeRegistry['iso_date']; 26 | 27 | // Type transformer (converts to boolean) 28 | boolean: RuleTypeDef; 29 | 30 | // Pass-through validators (validate but don't change type) 31 | has_methods: RuleTypeDef; 32 | hasMethods: RuleTypeRegistry['has_methods']; 33 | list_items_unique: RuleTypeDef; 34 | listItemsUnique: RuleTypeRegistry['list_items_unique']; 35 | list_length: RuleTypeDef; 36 | listLength: RuleTypeRegistry['list_length']; 37 | required_if: RuleTypeDef; 38 | requiredIf: RuleTypeRegistry['required_if']; 39 | } 40 | 41 | // Parameterized rules 42 | interface ParameterizedRuleRegistry { 43 | // 'is' outputs literal type of argument and has required effect 44 | is: ParameterizedRuleDef<'literal', true, false>; 45 | // 'instanceOf' outputs the instance type of the constructor argument 46 | instanceOf: ParameterizedRuleDef<'instance_of', false, false>; 47 | instance_of: ParameterizedRuleDef<'instance_of', false, false>; 48 | } 49 | } 50 | 51 | // ============================================================================ 52 | // Runtime Module Declaration 53 | // ============================================================================ 54 | 55 | type RuleFactory = ( 56 | ...args: Array 57 | ) => (value: any, params: any, outputArr: Array) => string | undefined; 58 | 59 | declare module 'livr-extra-rules' { 60 | /** 61 | * Extra rules to register with LIVR Validator. 62 | * 63 | * @example 64 | * ```typescript 65 | * import LIVR from 'livr'; 66 | * import extraRules from 'livr-extra-rules'; 67 | * import type { InferFromSchema } from 'livr/types'; 68 | * 69 | * LIVR.Validator.registerDefaultRules(extraRules); 70 | * 71 | * const schema = { 72 | * id: ['required', 'uuid'], 73 | * is_active: 'boolean', 74 | * status: { is: 'active' as const }, 75 | * } as const; 76 | * 77 | * type Data = InferFromSchema; 78 | * // { id: string; is_active?: boolean; status: 'active' } 79 | * ``` 80 | */ 81 | type LivrExtraRules = Record; 82 | const extraRules: LivrExtraRules; 83 | export = extraRules; 84 | } 85 | -------------------------------------------------------------------------------- /src/rules/iso_date.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const isoDateRe = /^(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9]))(T(2[0-3]|[01][0-9]):([0-5][0-9])(:([0-5][0-9])(\.[0-9]+)?)?(Z|[\+\-](2[0-3]|[01][0-9]):([0-5][0-9])))?$/; 3 | const dateRe = /^(\d{4})-([0-1][0-9])-([0-3][0-9])$/; 4 | const isoDateFormats = ['date', 'datetime']; 5 | const isoDateSpecialDates = ['yesterday', 'current', 'tomorrow']; 6 | 7 | function iso_date(params) { 8 | let min; 9 | let max; 10 | let format = 'date'; 11 | 12 | if (arguments.length > 1) { 13 | min = getDateFromParams(params.min, 'min'); 14 | max = getDateFromParams(params.max, 'max'); 15 | 16 | // max && console.log('max', params.max, (new Date(max)).toISOString()); 17 | if (params.format === 'datetime') format = params.format; 18 | } 19 | 20 | return (value, params, outputArr) => { 21 | if (util.isNoValue(value)) return; 22 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 23 | 24 | const matched = (value + '').match(isoDateRe); 25 | 26 | if (!matched || !isDateValid(matched[1])) return 'WRONG_DATE'; 27 | 28 | const epoch = Date.parse(value); 29 | if (!epoch && epoch !== 0) return 'WRONG_DATE'; 30 | 31 | if (min && epoch < min) return 'DATE_TOO_LOW'; 32 | if (max && epoch > max) return 'DATE_TOO_HIGH'; 33 | 34 | const date = new Date(epoch); 35 | 36 | if (format === 'date') { 37 | outputArr.push(date.toISOString().slice(0, 10)); 38 | } else { 39 | outputArr.push(date.toISOString()); 40 | } 41 | 42 | return; 43 | }; 44 | } 45 | 46 | function getDateFromParams(param, key) { 47 | if (!param) return; 48 | 49 | const matched = (param + '').match(isoDateRe); 50 | 51 | const i = isoDateSpecialDates.indexOf(param); 52 | 53 | if (i > -1) { 54 | date = new Date(); 55 | date.setDate(date.getDate() + (i - 1)); 56 | } else if (!matched || !isDateValid(matched[1])) { 57 | throw new Error('LIVR: wrong date in "' + key + '" parametr'); 58 | } else { 59 | const epoch = Date.parse(param); 60 | 61 | if (!epoch && epoch !== 0) { 62 | throw new Error('LIVR: wrong date in "' + key + '" parametr'); 63 | } 64 | 65 | date = new Date(epoch); 66 | } 67 | 68 | if (!matched || !matched[5]) { 69 | if (!matched) { 70 | date.setHours(0); 71 | date.setMinutes(0); 72 | date.setSeconds(0); 73 | date.setMilliseconds(0); 74 | } 75 | 76 | if (key === 'max') { 77 | date.setDate(date.getDate() + 1); 78 | date.setTime(date.getTime() - 1); 79 | } 80 | 81 | if (!matched) date.setTime(date.getTime() - date.getTimezoneOffset() * 60 * 1000); 82 | } 83 | 84 | return date.getTime(); 85 | } 86 | 87 | function isDateValid(value) { 88 | const matched = value.match(dateRe); 89 | 90 | if (matched) { 91 | const epoch = Date.parse(value); 92 | if (!epoch && epoch !== 0) return false; 93 | 94 | const d = new Date(epoch); 95 | d.setTime(d.getTime() + d.getTimezoneOffset() * 60 * 1000); 96 | 97 | if ( 98 | d.getFullYear() == matched[1] && 99 | d.getMonth() + 1 == +matched[2] && 100 | d.getDate() == +matched[3] 101 | ) { 102 | return true; 103 | } 104 | } 105 | 106 | return false; 107 | } 108 | 109 | module.exports = iso_date; 110 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Performance benchmark for LIVR extra rules 4 | */ 5 | 6 | const rules = require('./src/index'); 7 | 8 | const ITERATIONS = 100000; 9 | 10 | // Test data for each rule 11 | const testCases = { 12 | iso_date: { 13 | valid: ['2023-01-15', '2024-12-31', '1990-06-20', '2000-02-29'], 14 | invalid: ['2023-13-01', '2023-02-30', 'not-a-date', '2023/01/15'] 15 | }, 16 | ipv4: { 17 | valid: ['192.168.1.1', '10.0.0.1', '255.255.255.255', '0.0.0.0'], 18 | invalid: ['256.1.1.1', '192.168.1', 'abc.def.ghi.jkl', '01.02.03.04'] 19 | }, 20 | uuid: { 21 | valid: [ 22 | '550e8400-e29b-41d4-a716-446655440000', 23 | 'f47ac10b-58cc-4372-a567-0e02b2c3d479', 24 | '6ba7b810-9dad-41d4-80b4-00c04fd430c8' 25 | ], 26 | invalid: ['not-a-uuid', '550e8400-e29b-41d4-a716', 'zzzzzzzz-zzzz-4zzz-azzz-zzzzzzzzzzzz'] 27 | }, 28 | md5: { 29 | valid: ['d41d8cd98f00b204e9800998ecf8427e', '098f6bcd4621d373cade4e832627b4f6'], 30 | invalid: ['not-md5', 'd41d8cd98f00b204e9800998ecf8427', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'] 31 | }, 32 | mongo_id: { 33 | valid: ['507f1f77bcf86cd799439011', '5f50c31e8c3c6e001f3e6e8a'], 34 | invalid: ['not-mongo-id', '507f1f77bcf86cd79943901', 'zzzzzzzzzzzzzzzzzzzzzzzz'] 35 | }, 36 | base64: { 37 | valid: ['SGVsbG8gV29ybGQ=', 'dGVzdA==', 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo='], 38 | invalid: ['not base64!', 'SGVsbG8gV29ybGQ', '===='] 39 | }, 40 | credit_card: { 41 | valid: ['4532015112830366', '5425233430109903', '4916338506082832'], 42 | invalid: ['1234567890123456', '453201511283036', 'notacreditcard'] 43 | }, 44 | boolean: { 45 | valid: [true, false, 'true', 'false', 1, 0, '1', '0'], 46 | invalid: ['yes', 'no', 2, 'TRUE'] 47 | }, 48 | list_items_unique: { 49 | valid: [[1, 2, 3, 4, 5], ['a', 'b', 'c', 'd'], [1, 'a', 2, 'b']], 50 | invalid: [[1, 2, 2, 3], ['a', 'b', 'a'], [1, 1, 1]] 51 | }, 52 | list_length: { 53 | valid: [[1, 2, 3], [1, 2, 3, 4, 5], ['a', 'b', 'c', 'd']], 54 | invalid: [[1], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], []] 55 | }, 56 | is: { 57 | valid: ['expected_value', 'expected_value', 'expected_value'], 58 | invalid: ['wrong', 'another', 'nope'] 59 | } 60 | }; 61 | 62 | function benchmark(name, fn, data, iterations) { 63 | // Warmup 64 | for (let i = 0; i < 1000; i++) { 65 | for (const value of data) { 66 | fn(value, {}, []); 67 | } 68 | } 69 | 70 | const start = process.hrtime.bigint(); 71 | for (let i = 0; i < iterations; i++) { 72 | for (const value of data) { 73 | fn(value, {}, []); 74 | } 75 | } 76 | const end = process.hrtime.bigint(); 77 | const durationMs = Number(end - start) / 1e6; 78 | const opsPerSec = Math.round((iterations * data.length) / (durationMs / 1000)); 79 | return { durationMs, opsPerSec }; 80 | } 81 | 82 | function runBenchmarks() { 83 | console.log('LIVR Extra Rules Performance Benchmark'); 84 | console.log('='.repeat(60)); 85 | console.log(`Iterations per test: ${ITERATIONS.toLocaleString()}`); 86 | console.log(''); 87 | 88 | const results = {}; 89 | 90 | // iso_date 91 | const isoDateValidator = rules.iso_date(); 92 | results.iso_date_valid = benchmark('iso_date (valid)', isoDateValidator, testCases.iso_date.valid, ITERATIONS); 93 | results.iso_date_invalid = benchmark('iso_date (invalid)', isoDateValidator, testCases.iso_date.invalid, ITERATIONS); 94 | 95 | // ipv4 96 | const ipv4Validator = rules.ipv4(); 97 | results.ipv4_valid = benchmark('ipv4 (valid)', ipv4Validator, testCases.ipv4.valid, ITERATIONS); 98 | results.ipv4_invalid = benchmark('ipv4 (invalid)', ipv4Validator, testCases.ipv4.invalid, ITERATIONS); 99 | 100 | // uuid (v4) 101 | const uuidValidator = rules.uuid('v4'); 102 | results.uuid_valid = benchmark('uuid (valid)', uuidValidator, testCases.uuid.valid, ITERATIONS); 103 | results.uuid_invalid = benchmark('uuid (invalid)', uuidValidator, testCases.uuid.invalid, ITERATIONS); 104 | 105 | // md5 106 | const md5Validator = rules.md5(); 107 | results.md5_valid = benchmark('md5 (valid)', md5Validator, testCases.md5.valid, ITERATIONS); 108 | results.md5_invalid = benchmark('md5 (invalid)', md5Validator, testCases.md5.invalid, ITERATIONS); 109 | 110 | // mongo_id 111 | const mongoIdValidator = rules.mongo_id(); 112 | results.mongo_id_valid = benchmark('mongo_id (valid)', mongoIdValidator, testCases.mongo_id.valid, ITERATIONS); 113 | results.mongo_id_invalid = benchmark('mongo_id (invalid)', mongoIdValidator, testCases.mongo_id.invalid, ITERATIONS); 114 | 115 | // base64 116 | const base64Validator = rules.base64(); 117 | results.base64_valid = benchmark('base64 (valid)', base64Validator, testCases.base64.valid, ITERATIONS); 118 | results.base64_invalid = benchmark('base64 (invalid)', base64Validator, testCases.base64.invalid, ITERATIONS); 119 | 120 | // credit_card 121 | const creditCardValidator = rules.credit_card(); 122 | results.credit_card_valid = benchmark('credit_card (valid)', creditCardValidator, testCases.credit_card.valid, ITERATIONS); 123 | results.credit_card_invalid = benchmark('credit_card (invalid)', creditCardValidator, testCases.credit_card.invalid, ITERATIONS); 124 | 125 | // boolean 126 | const booleanValidator = rules.boolean(); 127 | results.boolean_valid = benchmark('boolean (valid)', booleanValidator, testCases.boolean.valid, ITERATIONS); 128 | results.boolean_invalid = benchmark('boolean (invalid)', booleanValidator, testCases.boolean.invalid, ITERATIONS); 129 | 130 | // list_items_unique 131 | const listItemsUniqueValidator = rules.list_items_unique(); 132 | results.list_items_unique_valid = benchmark('list_items_unique (valid)', listItemsUniqueValidator, testCases.list_items_unique.valid, ITERATIONS); 133 | results.list_items_unique_invalid = benchmark('list_items_unique (invalid)', listItemsUniqueValidator, testCases.list_items_unique.invalid, ITERATIONS); 134 | 135 | // list_length (min: 2, max: 6) 136 | const listLengthValidator = rules.list_length(2, 6); 137 | results.list_length_valid = benchmark('list_length (valid)', listLengthValidator, testCases.list_length.valid, ITERATIONS); 138 | results.list_length_invalid = benchmark('list_length (invalid)', listLengthValidator, testCases.list_length.invalid, ITERATIONS); 139 | 140 | // is 141 | const isValidator = rules.is('expected_value'); 142 | results.is_valid = benchmark('is (valid)', isValidator, testCases.is.valid, ITERATIONS); 143 | results.is_invalid = benchmark('is (invalid)', isValidator, testCases.is.invalid, ITERATIONS); 144 | 145 | // Print results 146 | console.log('Results:'); 147 | console.log('-'.repeat(60)); 148 | console.log(String('Rule').padEnd(30) + String('Time (ms)').padStart(12) + String('ops/sec').padStart(15)); 149 | console.log('-'.repeat(60)); 150 | 151 | for (const [name, result] of Object.entries(results)) { 152 | console.log( 153 | name.padEnd(30) + 154 | result.durationMs.toFixed(2).padStart(12) + 155 | result.opsPerSec.toLocaleString().padStart(15) 156 | ); 157 | } 158 | 159 | console.log('-'.repeat(60)); 160 | 161 | // Calculate totals 162 | const totalTime = Object.values(results).reduce((sum, r) => sum + r.durationMs, 0); 163 | console.log(`Total time: ${totalTime.toFixed(2)} ms`); 164 | 165 | return results; 166 | } 167 | 168 | runBenchmarks(); 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # livr-extra-rules 2 | 3 | > Extra validation rules for LIVR with zero dependencies 4 | 5 | [![npm version](https://badge.fury.io/js/livr-extra-rules.svg)](https://badge.fury.io/js/livr-extra-rules) 6 | [![npm downloads](https://img.shields.io/npm/dm/livr-extra-rules.svg)](https://www.npmjs.com/package/livr-extra-rules) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 8 | [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/koorchik/js-livr-extra-rules/badge.svg?targetFile=package.json)](https://snyk.io/test/github/koorchik/js-livr-extra-rules?targetFile=package.json) 10 | 11 | --- 12 | 13 | ## Table of Contents 14 | 15 | - [Highlights](#highlights) 16 | - [Installation](#installation) 17 | - [Quick Start](#quick-start) 18 | - [Rules Overview](#rules-overview) 19 | - [Rule Documentation](#rule-documentation) 20 | - [`ipv4`](#ipv4) 21 | - [`boolean`](#boolean) 22 | - [`is`](#is) 23 | - [`credit_card`](#credit_card) 24 | - [`uuid`](#uuid) 25 | - [`mongo_id`](#mongo_id) 26 | - [`list_length`](#list_length) 27 | - [`list_items_unique`](#list_items_unique) 28 | - [`base64`](#base64) 29 | - [`md5`](#md5) 30 | - [`iso_date`](#iso_date) 31 | - [`required_if`](#required_if) 32 | - [`instance_of`](#instance_of) 33 | - [`has_methods`](#has_methods) 34 | - [Contributing](#contributing) 35 | - [Documentation](#documentation) 36 | - [Contributors](#contributors) 37 | - [License](#license) 38 | 39 | --- 40 | 41 | ## Highlights 42 | 43 | - **Zero Dependencies** — Lighter builds, easier to maintain high level of security 44 | - **TypeScript Support** — Full type definitions included 45 | - **14 Extra Rules** — Credit cards, UUIDs, dates, IP addresses, and more 46 | 47 | ## Installation 48 | 49 | ```sh 50 | npm install livr livr-extra-rules 51 | ``` 52 | 53 | ## Quick Start 54 | 55 | ```js 56 | import LIVR from 'livr'; 57 | import extraRules from 'livr-extra-rules'; 58 | 59 | LIVR.Validator.registerDefaultRules(extraRules); 60 | ``` 61 | 62 | Works with AsyncValidator too: 63 | 64 | ```js 65 | import LIVR from 'livr/async'; 66 | import extraRules from 'livr-extra-rules'; 67 | 68 | LIVR.AsyncValidator.registerDefaultRules(extraRules); 69 | ``` 70 | 71 | --- 72 | 73 | ## Rules Overview 74 | 75 | | Rule | Description | Error Code(s) | 76 | |------|-------------|---------------| 77 | | [`ipv4`](#ipv4) | Validates IPv4 addresses | `NOT_IP` | 78 | | [`boolean`](#boolean) | Checks for true/false values | `NOT_BOOLEAN` | 79 | | [`is`](#is) | Exact value match | `REQUIRED`, `NOT_ALLOWED_VALUE` | 80 | | [`credit_card`](#credit_card) | Validates credit card numbers (Luhn) | `WRONG_CREDIT_CARD_NUMBER` | 81 | | [`uuid`](#uuid) | Validates UUID (v1-v5) | `NOT_UUID` | 82 | | [`mongo_id`](#mongo_id) | Validates MongoDB ObjectId | `NOT_ID` | 83 | | [`list_length`](#list_length) | Validates array length | `FORMAT_ERROR`, `TOO_FEW_ITEMS`, `TOO_MANY_ITEMS` | 84 | | [`list_items_unique`](#list_items_unique) | Checks array uniqueness | `FORMAT_ERROR`, `NOT_UNIQUE_ITEMS`, `INCOMPARABLE_ITEMS` | 85 | | [`base64`](#base64) | Validates base64 strings | `MALFORMED_BASE64` | 86 | | [`md5`](#md5) | Validates MD5 hash strings | `NOT_MD5` | 87 | | [`iso_date`](#iso_date) | Extended ISO date validation | `WRONG_DATE`, `DATE_TOO_LOW`, `DATE_TOO_HIGH` | 88 | | [`required_if`](#required_if) | Conditional required field | `REQUIRED` | 89 | | [`instance_of`](#instance_of) | Class instance check | `WRONG_INSTANCE` | 90 | | [`has_methods`](#has_methods) | Object method check | `NOT_HAVING_METHOD [method]` | 91 | 92 | --- 93 | 94 | ## Rule Documentation 95 | 96 | ### `ipv4` 97 | 98 | Validates IPv4 addresses. 99 | 100 | ```js 101 | { 102 | field: 'ipv4' 103 | } 104 | ``` 105 | 106 | **Error code:** `NOT_IP` 107 | 108 | ### `boolean` 109 | 110 | Checks that the value is true or false. 111 | 112 | - **True values:** `true`, `1`, `'1'` 113 | - **False values:** `false`, `0`, `'0'` 114 | 115 | String values (except empty string) will return error `NOT_BOOLEAN`. 116 | 117 | Return value will be converted to JavaScript boolean values — `true` or `false`. 118 | 119 | ```js 120 | { 121 | field: 'boolean' 122 | } 123 | ``` 124 | 125 | **Error code:** `NOT_BOOLEAN` 126 | 127 | ### `is` 128 | 129 | Checks the presence of the value and its correspondence to the specified value. 130 | 131 | ```js 132 | { 133 | field: { 'is': 'some value' } 134 | } 135 | ``` 136 | 137 | **Error codes:** `REQUIRED`, `NOT_ALLOWED_VALUE` 138 | 139 | ### `credit_card` 140 | 141 | Checks that the value is a credit card number with [Luhn Algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm). 142 | 143 | ```js 144 | { 145 | field: 'credit_card' 146 | } 147 | ``` 148 | 149 | **Error code:** `WRONG_CREDIT_CARD_NUMBER` 150 | 151 | ### `uuid` 152 | 153 | Validates UUID strings (versions 1-5). 154 | 155 | ```js 156 | { 157 | field1: 'uuid', // default v4 158 | field2: { uuid: 'v1' }, 159 | field3: { uuid: 'v2' }, 160 | field4: { uuid: 'v3' }, 161 | field5: { uuid: 'v4' }, 162 | field6: { uuid: 'v5' } 163 | } 164 | ``` 165 | 166 | **Error code:** `NOT_UUID` 167 | 168 | ### `mongo_id` 169 | 170 | Checks that the value looks like a MongoDB ObjectId. 171 | 172 | ```js 173 | { 174 | field: 'mongo_id' 175 | } 176 | ``` 177 | 178 | **Error code:** `NOT_ID` 179 | 180 | ### `list_length` 181 | 182 | Checks that the value is a list and contains the required number of elements. You can pass an exact number or a range. 183 | 184 | > **Note:** Don't forget about the `required` rule if you want the field to be required. 185 | 186 | ```js 187 | { 188 | // List is required and should contain exactly 10 items 189 | list1: ['required', { list_length: 10 }], 190 | 191 | // List is not required but if present, should contain exactly 10 items 192 | list2: { list_length: 10 }, 193 | 194 | // List is not required but if present, should have from 3 to 10 items 195 | list3: { list_length: [3, 10] } 196 | } 197 | ``` 198 | 199 | **Error codes:** `FORMAT_ERROR`, `TOO_FEW_ITEMS`, `TOO_MANY_ITEMS` 200 | 201 | ### `list_items_unique` 202 | 203 | Checks that items in a list are unique. The rule checks string representations of values and supports only primitive values. 204 | 205 | - If the value is not an array → `FORMAT_ERROR` 206 | - If the value is not primitive (array, object) → `INCOMPARABLE_ITEMS` 207 | 208 | ```js 209 | { 210 | list: 'list_items_unique' 211 | } 212 | ``` 213 | 214 | **Error codes:** `FORMAT_ERROR`, `NOT_UNIQUE_ITEMS`, `INCOMPARABLE_ITEMS` 215 | 216 | ### `base64` 217 | 218 | Validates base64 encoded strings. 219 | 220 | ```js 221 | { 222 | field1: 'base64', // padding is required (default) 223 | field2: { base64: 'relaxed' } // padding is optional 224 | } 225 | ``` 226 | 227 | **Error code:** `MALFORMED_BASE64` 228 | 229 | ### `md5` 230 | 231 | Validates MD5 hash strings. 232 | 233 | ```js 234 | { 235 | field: 'md5' 236 | } 237 | ``` 238 | 239 | **Error code:** `NOT_MD5` 240 | 241 | ### `iso_date` 242 | 243 | Compatible with the standard `iso_date` rule (and will redefine it) but allows extra params — `min` and `max` dates. 244 | 245 | Special date values: `current`, `yesterday`, `tomorrow` — useful for checking if a date is in the future or past. 246 | 247 | ```js 248 | { 249 | date1: 'iso_date', 250 | date2: { iso_date: { min: '2017-10-15' } }, 251 | date3: { iso_date: { max: '2017-10-30' } }, 252 | date4: { iso_date: { min: '2017-10-15T15:30Z', max: '2017-10-30', format: 'datetime' } }, 253 | date5: { iso_date: { min: 'current', max: 'tomorrow' } }, 254 | date6: { iso_date: { format: 'datetime' } } 255 | } 256 | ``` 257 | 258 | **Options:** 259 | 260 | | Option | Description | Default | 261 | |--------|-------------|---------| 262 | | `min` | ISO 8601 date/datetime, `current`, `tomorrow`, `yesterday` | — | 263 | | `max` | ISO 8601 date/datetime, `current`, `tomorrow`, `yesterday` | — | 264 | | `format` | `date` or `datetime` | `date` | 265 | 266 | **Date boundary behavior:** 267 | 268 | If you pass only a date (without time) to `min` or `max` and the expected format is `datetime`: 269 | - `min` starts from the beginning of the min date 270 | - `max` ends at the end of the max date 271 | 272 | If you pass the time along with the date, you need to specify the time zone. 273 | 274 | **Error codes:** `WRONG_DATE`, `DATE_TOO_LOW`, `DATE_TOO_HIGH` 275 | 276 | ### `required_if` 277 | 278 | Checks that the value is present if another field is present and has a specific value. 279 | 280 | **Simple example:** 281 | 282 | ```js 283 | { 284 | sendMeEmails: { one_of: [0, 1] }, 285 | email: { required_if: { sendMeEmails: '1' } } 286 | } 287 | ``` 288 | 289 | **With JSON pointer:** 290 | 291 | ```js 292 | { 293 | address: { 294 | nested_object: { 295 | city: 'required', 296 | street: 'required' 297 | } 298 | }, 299 | email: { required_if: { 'address/city': 'Kyiv' } } 300 | } 301 | ``` 302 | 303 | > **Note:** You cannot access parent fields with JSON pointers here, only siblings and nested values. 304 | 305 | **Error code:** `REQUIRED` 306 | 307 | ### `instance_of` 308 | 309 | Checks that the value is an instanceof a class. 310 | 311 | > **Note:** This rule is JS-specific and not serializable but can be useful for runtime validations. 312 | 313 | ```js 314 | class Dog {} 315 | 316 | { 317 | dog1: { instance_of: Dog } 318 | } 319 | ``` 320 | 321 | **Error code:** `WRONG_INSTANCE` 322 | 323 | ### `has_methods` 324 | 325 | Checks that the value is an object which has all required methods. 326 | 327 | > **Note:** This rule is JS-specific and not serializable but can be useful for runtime validations. 328 | 329 | ```js 330 | { 331 | dog1: { has_methods: 'bark' }, 332 | dog2: { has_methods: ['bark', 'getName'] } 333 | } 334 | ``` 335 | 336 | **Error code:** `NOT_HAVING_METHOD [${method}]` (e.g., `NOT_HAVING_METHOD [bark]`) 337 | 338 | --- 339 | 340 | ## Contributing 341 | 342 | To add a new rule: 343 | 344 | 1. Create a new file in `src/rules/` (see existing rules for reference) 345 | 2. Export the rule in `src/index.js` 346 | 3. Add positive tests in `tests/test_suite/positive/your_rule_name/` 347 | 4. Add negative tests in `tests/test_suite/negative/your_rule_name/` 348 | 5. Update this README 349 | 350 | ## Documentation 351 | 352 | - [LIVR for JavaScript](https://www.npmjs.com/package/livr) 353 | - [Official LIVR documentation](http://livr-spec.org/) 354 | 355 | --- 356 | 357 | ## Contributors 358 | 359 | [![@vira-khdr](https://github.com/vira-khdr.png?size=40)](https://github.com/vira-khdr) [@vira-khdr](https://github.com/vira-khdr) 360 | 361 | --- 362 | 363 | ## License 364 | 365 | MIT 366 | -------------------------------------------------------------------------------- /test-types.ts: -------------------------------------------------------------------------------- 1 | // Type inference tests for livr-extra-rules 2 | // Run with: npx tsc --noEmit 3 | 4 | import type { InferFromSchema } from 'livr/types'; 5 | 6 | // Import type augmentations 7 | import './types'; 8 | 9 | // ============================================================================ 10 | // Type Testing Utilities 11 | // ============================================================================ 12 | 13 | type Expect = T; 14 | type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; 15 | 16 | // ============================================================================ 17 | // String Validators (output: string) 18 | // ============================================================================ 19 | 20 | // base64 21 | { 22 | const schema = { field: 'base64' } as const; 23 | type Result = InferFromSchema; 24 | type _test = Expect>; 25 | } 26 | 27 | // credit_card 28 | { 29 | const schema = { field: 'credit_card' } as const; 30 | type Result = InferFromSchema; 31 | type _test = Expect>; 32 | } 33 | 34 | // creditCard (camelCase alias) 35 | { 36 | const schema = { field: 'creditCard' } as const; 37 | type Result = InferFromSchema; 38 | type _test = Expect>; 39 | } 40 | 41 | // ipv4 42 | { 43 | const schema = { field: 'ipv4' } as const; 44 | type Result = InferFromSchema; 45 | type _test = Expect>; 46 | } 47 | 48 | // md5 49 | { 50 | const schema = { field: 'md5' } as const; 51 | type Result = InferFromSchema; 52 | type _test = Expect>; 53 | } 54 | 55 | // mongo_id 56 | { 57 | const schema = { field: 'mongo_id' } as const; 58 | type Result = InferFromSchema; 59 | type _test = Expect>; 60 | } 61 | 62 | // mongoId (camelCase alias) 63 | { 64 | const schema = { field: 'mongoId' } as const; 65 | type Result = InferFromSchema; 66 | type _test = Expect>; 67 | } 68 | 69 | // uuid 70 | { 71 | const schema = { field: 'uuid' } as const; 72 | type Result = InferFromSchema; 73 | type _test = Expect>; 74 | } 75 | 76 | // iso_date 77 | { 78 | const schema = { field: 'iso_date' } as const; 79 | type Result = InferFromSchema; 80 | type _test = Expect>; 81 | } 82 | 83 | // isoDate (camelCase alias) 84 | { 85 | const schema = { field: 'isoDate' } as const; 86 | type Result = InferFromSchema; 87 | type _test = Expect>; 88 | } 89 | 90 | // ============================================================================ 91 | // Type Transformer (output: boolean) 92 | // ============================================================================ 93 | 94 | // boolean 95 | { 96 | const schema = { field: 'boolean' } as const; 97 | type Result = InferFromSchema; 98 | type _test = Expect>; 99 | } 100 | 101 | // ============================================================================ 102 | // Pass-through Validators (output: unknown) 103 | // ============================================================================ 104 | 105 | // has_methods 106 | { 107 | const schema = { field: 'has_methods' } as const; 108 | type Result = InferFromSchema; 109 | type _test = Expect>; 110 | } 111 | 112 | // hasMethods (camelCase alias) 113 | { 114 | const schema = { field: 'hasMethods' } as const; 115 | type Result = InferFromSchema; 116 | type _test = Expect>; 117 | } 118 | 119 | // list_items_unique 120 | { 121 | const schema = { field: 'list_items_unique' } as const; 122 | type Result = InferFromSchema; 123 | type _test = Expect>; 124 | } 125 | 126 | // listItemsUnique (camelCase alias) 127 | { 128 | const schema = { field: 'listItemsUnique' } as const; 129 | type Result = InferFromSchema; 130 | type _test = Expect>; 131 | } 132 | 133 | // list_length 134 | { 135 | const schema = { field: { list_length: 3 } } as const; 136 | type Result = InferFromSchema; 137 | type _test = Expect>; 138 | } 139 | 140 | // listLength (camelCase alias) 141 | { 142 | const schema = { field: { listLength: 3 } } as const; 143 | type Result = InferFromSchema; 144 | type _test = Expect>; 145 | } 146 | 147 | // required_if 148 | { 149 | const schema = { field: { required_if: { other: 'value' } } } as const; 150 | type Result = InferFromSchema; 151 | type _test = Expect>; 152 | } 153 | 154 | // requiredIf (camelCase alias) 155 | { 156 | const schema = { field: { requiredIf: { other: 'value' } } } as const; 157 | type Result = InferFromSchema; 158 | type _test = Expect>; 159 | } 160 | 161 | // ============================================================================ 162 | // Parameterized Rules 163 | // ============================================================================ 164 | 165 | // is - outputs literal type with required effect 166 | { 167 | const schema = { field: { is: 'active' as const } } as const; 168 | type Result = InferFromSchema; 169 | type _test = Expect>; 170 | } 171 | 172 | // is - with number literal 173 | { 174 | const schema = { field: { is: 42 as const } } as const; 175 | type Result = InferFromSchema; 176 | type _test = Expect>; 177 | } 178 | 179 | // instanceOf - outputs instance type 180 | { 181 | const schema = { field: { instanceOf: Date } } as const; 182 | type Result = InferFromSchema; 183 | type _test = Expect>; 184 | } 185 | 186 | // instance_of (snake_case) - outputs instance type 187 | { 188 | const schema = { field: { instance_of: Date } } as const; 189 | type Result = InferFromSchema; 190 | type _test = Expect>; 191 | } 192 | 193 | // instanceOf with custom class 194 | { 195 | class MyClass { 196 | value: number = 0; 197 | } 198 | const schema = { field: { instanceOf: MyClass } } as const; 199 | type Result = InferFromSchema; 200 | type _test = Expect>; 201 | } 202 | 203 | // ============================================================================ 204 | // Combined with Core LIVR Rules 205 | // ============================================================================ 206 | 207 | // required + string validator 208 | { 209 | const schema = { field: ['required', 'uuid'] } as const; 210 | type Result = InferFromSchema; 211 | type _test = Expect>; 212 | } 213 | 214 | // required + boolean 215 | { 216 | const schema = { field: ['required', 'boolean'] } as const; 217 | type Result = InferFromSchema; 218 | type _test = Expect>; 219 | } 220 | 221 | // nested_object with extra rules 222 | { 223 | const schema = { 224 | user: { 225 | nested_object: { 226 | id: ['required', 'uuid'], 227 | email_hash: 'md5', 228 | is_active: 'boolean', 229 | }, 230 | }, 231 | } as const; 232 | type Result = InferFromSchema; 233 | type _test = Expect< 234 | Equal< 235 | Result, 236 | { 237 | user?: { 238 | id: string; 239 | email_hash?: string; 240 | is_active?: boolean; 241 | }; 242 | } 243 | > 244 | >; 245 | } 246 | 247 | // ============================================================================ 248 | // Complex Schemas 249 | // ============================================================================ 250 | 251 | // Real-world example: User profile 252 | { 253 | const schema = { 254 | id: ['required', 'mongo_id'], 255 | avatar: 'base64', 256 | credit_card: 'credit_card', 257 | ip_address: 'ipv4', 258 | password_hash: 'md5', 259 | session_id: 'uuid', 260 | birth_date: 'iso_date', 261 | is_verified: 'boolean', 262 | status: { is: 'active' as const }, 263 | } as const; 264 | type Result = InferFromSchema; 265 | type _test = Expect< 266 | Equal< 267 | Result, 268 | { 269 | id: string; 270 | avatar?: string; 271 | credit_card?: string; 272 | ip_address?: string; 273 | password_hash?: string; 274 | session_id?: string; 275 | birth_date?: string; 276 | is_verified?: boolean; 277 | status: 'active'; 278 | } 279 | > 280 | >; 281 | } 282 | 283 | // CamelCase variant of the same schema 284 | { 285 | const schema = { 286 | id: ['required', 'mongoId'], 287 | avatar: 'base64', 288 | creditCard: 'creditCard', 289 | ipAddress: 'ipv4', 290 | passwordHash: 'md5', 291 | sessionId: 'uuid', 292 | birthDate: 'isoDate', 293 | isVerified: 'boolean', 294 | status: { is: 'active' as const }, 295 | } as const; 296 | type Result = InferFromSchema; 297 | type _test = Expect< 298 | Equal< 299 | Result, 300 | { 301 | id: string; 302 | avatar?: string; 303 | creditCard?: string; 304 | ipAddress?: string; 305 | passwordHash?: string; 306 | sessionId?: string; 307 | birthDate?: string; 308 | isVerified?: boolean; 309 | status: 'active'; 310 | } 311 | > 312 | >; 313 | } 314 | 315 | // Multiple rules per field 316 | { 317 | const schema = { 318 | tags: ['list_items_unique', { list_length: 5 }], 319 | } as const; 320 | type Result = InferFromSchema; 321 | type _test = Expect>; 322 | } 323 | --------------------------------------------------------------------------------