├── .gitignore ├── scripts ├── browser_build_entry-async.js └── browser_build_entry-sync.js ├── t ├── test_suite │ ├── negative │ │ ├── 02-not_empty │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 01-required │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 22-not_empty_list │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 27-any_object │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ ├── 26-string │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ ├── 19-list_of │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 18-nested_object │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 20-list_of_objects │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ └── input.json │ │ ├── 23-url │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ └── input.json │ │ ├── 05-max_length │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 04-min_length │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 06-length_equal │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 07-length_between │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 13-max_number │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 17-equal_to_field │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 03-one_of │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 08-like │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 14-min_number │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 09-integer │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 11-decimal │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 24-iso_date │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 10-positive_integer │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 29-or │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 15-number_beetween │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 12-positive_decimal │ │ │ ├── input.json │ │ │ ├── errors.json │ │ │ └── rules.json │ │ ├── 28-variable_object │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 25-eq │ │ │ ├── input.json │ │ │ ├── rules.json │ │ │ └── errors.json │ │ ├── 21-list_of_different_objects │ │ │ ├── errors.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ └── 16-email │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ └── input.json │ ├── positive │ │ ├── 02-not_empty │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 09-integer │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 13-max_number │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 14-min_number │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 10-positive_integer │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 01-required │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 23-url │ │ │ ├── rules.json │ │ │ ├── output.json │ │ │ └── input.json │ │ ├── 15-number_between │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 17-equal_to_field │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 22-not_empty_list │ │ │ ├── output.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── 26-string │ │ │ ├── output.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── 11-decimal │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 08-like │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 12-positive_decimal │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 34-leave_only │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 04-min_length │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 06-length_equal │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 33-remove │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 27-any_object │ │ │ ├── input.json │ │ │ ├── output.json │ │ │ └── rules.json │ │ ├── 05-max_length │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 19-list_of │ │ │ ├── input.json │ │ │ ├── output.json │ │ │ └── rules.json │ │ ├── 25-eq │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 07-length_between │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 24-iso_date │ │ │ ├── output.json │ │ │ ├── rules.json │ │ │ └── input.json │ │ ├── 03-one_of │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 30-trim │ │ │ ├── rules.json │ │ │ ├── output.json │ │ │ └── input.json │ │ ├── 32-to_uc │ │ │ ├── input.json │ │ │ ├── output.json │ │ │ └── rules.json │ │ ├── 18-nested_object │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 31-to_lc │ │ │ ├── input.json │ │ │ ├── output.json │ │ │ └── rules.json │ │ ├── 35-default │ │ │ ├── input.json │ │ │ ├── output.json │ │ │ └── rules.json │ │ ├── 20-list_of_objects │ │ │ ├── rules.json │ │ │ ├── input.json │ │ │ └── output.json │ │ ├── 28-variable_object │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 29-or │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ ├── 21-list_of_different_objects │ │ │ ├── output.json │ │ │ ├── input.json │ │ │ └── rules.json │ │ └── 16-email │ │ │ ├── rules.json │ │ │ ├── output.json │ │ │ └── input.json │ ├── aliases_negative │ │ ├── 02-address │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── aliases.json │ │ ├── 03-adult_age_in_user │ │ │ ├── rules.json │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ └── aliases.json │ │ └── 01-adult_age │ │ │ ├── errors.json │ │ │ ├── input.json │ │ │ ├── aliases.json │ │ │ └── rules.json │ └── aliases_positive │ │ ├── 02-address │ │ ├── rules.json │ │ ├── output.json │ │ ├── input.json │ │ └── aliases.json │ │ ├── 03-adult_age_in_user │ │ ├── rules.json │ │ ├── output.json │ │ ├── input.json │ │ └── aliases.json │ │ └── 01-adult_age │ │ ├── output.json │ │ ├── input.json │ │ ├── aliases.json │ │ └── rules.json ├── tests-sync │ ├── 04-custom_aliases.js │ ├── 06-camelize_rule_names.js │ ├── 02-auto_trim.js │ ├── 03-custom_filters.js │ ├── 05-rules_replacement.js │ └── 01-test_suite.js └── tests-async │ ├── 04-custom_aliases.js │ ├── 06-camelize_rule_names.js │ ├── 02-auto_trim.js │ ├── 03-custom_filters.js │ ├── 05-rules_replacement.js │ └── 01-test_suite.js ├── .prettierrc.json ├── .travis.yml ├── lib ├── LIVR.d.ts ├── rules │ ├── special │ │ ├── url.d.ts │ │ ├── email.d.ts │ │ ├── iso_date.d.ts │ │ ├── equal_to_field.d.ts │ │ ├── equal_to_field.js │ │ ├── email.js │ │ ├── url.js │ │ └── iso_date.js │ ├── modifiers │ │ ├── trim.d.ts │ │ ├── remove.d.ts │ │ ├── default.js │ │ ├── to_lc.d.ts │ │ ├── to_uc.d.ts │ │ ├── leave_only.d.ts │ │ ├── trim.js │ │ ├── default.d.ts │ │ ├── to_uc.js │ │ ├── to_lc.js │ │ ├── remove.js │ │ └── leave_only.js │ ├── string │ │ ├── like.d.ts │ │ ├── string.d.ts │ │ ├── max_length.d.ts │ │ ├── min_length.d.ts │ │ ├── length_equal.d.ts │ │ ├── eq.d.ts │ │ ├── length_between.d.ts │ │ ├── string.js │ │ ├── one_of.d.ts │ │ ├── max_length.js │ │ ├── min_length.js │ │ ├── length_equal.js │ │ ├── length_between.js │ │ ├── eq.js │ │ ├── like.js │ │ └── one_of.js │ ├── common │ │ ├── required.d.ts │ │ ├── not_empty.js │ │ ├── required.js │ │ ├── any_object.js │ │ ├── not_empty.d.ts │ │ ├── not_empty_list.js │ │ ├── any_object.d.ts │ │ └── not_empty_list.d.ts │ ├── numeric │ │ ├── decimal.d.ts │ │ ├── integer.d.ts │ │ ├── max_number.d.ts │ │ ├── min_number.d.ts │ │ ├── number_between.d.ts │ │ ├── positive_decimal.d.ts │ │ ├── positive_integer.d.ts │ │ ├── integer.js │ │ ├── max_number.js │ │ ├── min_number.js │ │ ├── positive_decimal.js │ │ ├── positive_integer.js │ │ ├── decimal.js │ │ └── number_between.js │ ├── meta │ │ ├── nested_object.d.ts │ │ ├── list_of_objects.d.ts │ │ ├── list_of.d.ts │ │ ├── nested_object.js │ │ ├── or.d.ts │ │ ├── variable_object.d.ts │ │ ├── or.js │ │ ├── list_of_different_objects.d.ts │ │ ├── variable_object.js │ │ ├── list_of_objects.js │ │ ├── list_of.js │ │ └── list_of_different_objects.js │ └── meta-async │ │ ├── nested_object.js │ │ ├── or.js │ │ ├── variable_object.js │ │ ├── list_of_objects.js │ │ ├── list_of.js │ │ └── list_of_different_objects.js ├── util.js ├── LIVR.js ├── AsyncValidator.js ├── Validator.js └── BaseValidator.js ├── tsconfig.json ├── LICENSE ├── CHANGELOG.md ├── .devcontainer └── devcontainer.json ├── examples └── simple.ts ├── package.json ├── async.js ├── CLAUDE.md └── benchmarks └── bench.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output -------------------------------------------------------------------------------- /scripts/browser_build_entry-async.js: -------------------------------------------------------------------------------- 1 | window.LIVR = require('../async'); 2 | -------------------------------------------------------------------------------- /scripts/browser_build_entry-sync.js: -------------------------------------------------------------------------------- 1 | window.LIVR = require('../lib/LIVR'); 2 | -------------------------------------------------------------------------------- /t/test_suite/negative/02-not_empty/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "CANNOT_BE_EMPTY" 3 | } -------------------------------------------------------------------------------- /.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 | 8 | -------------------------------------------------------------------------------- /lib/LIVR.d.ts: -------------------------------------------------------------------------------- 1 | // lib/LIVR.d.ts - Type declaration for lib/LIVR.js 2 | import LIVR = require('../types'); 3 | export = LIVR; 4 | -------------------------------------------------------------------------------- /t/test_suite/positive/02-not_empty/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": null, 4 | "salary": 0 5 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/02-address/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "address", 3 | "address_custom_error": "address_with_custom_error" 4 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/03-adult_age_in_user/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "user", 3 | "user_custom_error": ["user_with_custom_error"] 4 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/02-address/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "address", 3 | "address_custom_error": "address_with_custom_error" 4 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/03-adult_age_in_user/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "user", 3 | "user_custom_error": ["user_with_custom_error"] 4 | } -------------------------------------------------------------------------------- /t/test_suite/negative/01-required/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "REQUIRED", 3 | "last_name": "REQUIRED", 4 | "middle_name": "REQUIRED" 5 | } -------------------------------------------------------------------------------- /t/test_suite/negative/22-not_empty_list/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list1": [], 3 | "list2": [], 4 | "not_list": {}, 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/02-not_empty/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": null, 4 | "age": "25", 5 | "salary": 0 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/09-integer/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 0, 4 | "number3": -1, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/13-max_number/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 15, 4 | "number3": -10, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/14-min_number/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 25, 4 | "number3": 40, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/10-positive_integer/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 999999, 4 | "number3": 1, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/01-required/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some", 5 | "salary": 0 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/23-url/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "url", 3 | "url2": { "url": [] }, 4 | "url3": [ { "url": [] } ], 5 | "empty_field": "url" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/09-integer/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": 0, 4 | "number3": -1, 5 | "empty_field": "", 6 | "extra_field": "100" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/13-max_number/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": 15, 4 | "number3": -10, 5 | "empty_field": "", 6 | "extra_field": "100" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/14-min_number/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": 25, 4 | "number3": 40, 5 | "empty_field": "", 6 | "extra_field": "100" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/15-number_between/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 25.55, 4 | "number3": 0, 5 | "number4": 1, 6 | "empty_field": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/17-equal_to_field/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": "some value", 3 | "field2": "some value", 4 | "field3": "some value", 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/22-not_empty_list/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_with_numbers": [0], 3 | "list_with_objects": [{},{"key": "123"}], 4 | "list_with_arrays": [[1,2,3]] 5 | } -------------------------------------------------------------------------------- /t/test_suite/positive/26-string/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "number1": "2", 5 | "number2": "2", 6 | "empty_city": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/01-required/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some", 5 | "age": "25", 6 | "salary": 0 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/11-decimal/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10.12, 3 | "number2": 0.55, 4 | "number3": -1.10, 5 | "number4": -3.00, 6 | "empty_field": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/08-like/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Ivanovich", 5 | "age": "35", 6 | "empty_name": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/10-positive_integer/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": 999999, 4 | "number3": 1, 5 | "empty_field": "", 6 | "extra_field": "100" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/12-positive_decimal/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": 10, 3 | "number2": 120.11, 4 | "number3": 99999.9999, 5 | "number4": 0.01, 6 | "empty_field": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/34-leave_only/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "Vl l Vl", 3 | "object": {"string_value": "a-vaa"}, 4 | "list_of_string_values": ["", {"key": "123"}, "23"] 5 | } -------------------------------------------------------------------------------- /t/test_suite/positive/01-required/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "required", 3 | "last_name": ["required"], 4 | "middle_name": [ { "required": [] } ], 5 | "salary": { "required": [] } 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/02-not_empty/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "not_empty", 3 | "last_name": ["not_empty"], 4 | "middle_name": [ { "not_empty": [] } ], 5 | "salary": { "required": [] } 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/22-not_empty_list/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_with_numbers": "not_empty_list", 3 | "list_with_objects": [{ "not_empty_list" : [] }], 4 | "list_with_arrays": "not_empty_list" 5 | } -------------------------------------------------------------------------------- /t/test_suite/positive/26-string/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "string", 3 | "city2": [{ "string": [] }], 4 | "number1": "string", 5 | "number2": "string", 6 | "empty_city": "string" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/01-adult_age/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "age3": "TOO_LOW", 3 | "age4": "NOT_POSITIVE_INTEGER", 4 | "age3_custom_error": "WRONG_AGE", 5 | "age4_custom_error": "WRONG_AGE" 6 | } -------------------------------------------------------------------------------- /t/test_suite/negative/27-any_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_string": "test", 3 | "value_is_number": 0, 4 | "value_is_array": ["test", 1], 5 | "value_is_empty_array": [] 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/04-min_length/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": "1111", 6 | "empty_name": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/06-length_equal/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": "1111", 6 | "empty_name": "" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/22-not_empty_list/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_with_numbers": [0], 3 | "list_with_objects": [{},{"key": "123"}], 4 | "list_with_arrays": [[1,2,3]], 5 | "extra_field": [1,2,3] 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/26-string/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "number1": "2", 5 | "number2": 2, 6 | "empty_city": "", 7 | "extra_city": "New York" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/33-remove/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "aluevaluealue", 3 | "object": {"string_value": "Vluelue Vlue"}, 4 | "list_of_string_values": ["value1", {"key": "123"}, "value"] 5 | } -------------------------------------------------------------------------------- /t/test_suite/negative/26-string/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_hash": {"test": 1}, 3 | "value_is_empty_hash": {}, 4 | "value_is_array": ["test", 1], 5 | "value_is_empty_array": [] 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/11-decimal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10.12", 3 | "number2": 0.55, 4 | "number3": -1.10, 5 | "number4": -3.00, 6 | "empty_field": "", 7 | "extra_field": "100" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/15-number_between/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": "25.55", 4 | "number3": "0", 5 | "number4": 1, 6 | "empty_field": "", 7 | "extra_field": "100" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/17-equal_to_field/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": "some value", 3 | "field2": "some value", 4 | "field3": "some value", 5 | "empty_field": "", 6 | "extra_field": "some value" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/27-any_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "object1": {"email": "user@mail.com"}, 3 | "object2": {"id": 123, "address": {"city" : "Kiev"}}, 4 | "object3": {}, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/27-any_object/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "object1": {"email": "user@mail.com"}, 3 | "object2": {"id": 123, "address": {"city" : "Kiev"}}, 4 | "object3": {}, 5 | "empty_field": "" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/negative/19-list_of/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": [ -10, "", 50, 120 ], 3 | "product_ids2": [ -10, "", 50, 120 ], 4 | "product_ids3": [ -10, "", 50, 120 ], 5 | "user_ids": "not an array" 6 | } -------------------------------------------------------------------------------- /t/test_suite/negative/26-string/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_hash": "string", 3 | "value_is_empty_hash": "string", 4 | "value_is_array": "string", 5 | "value_is_empty_array": "string" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/11-decimal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "decimal", 3 | "number2": ["decimal"], 4 | "number3": [ { "decimal": [] } ], 5 | "number4": "decimal", 6 | "empty_field": "decimal" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/33-remove/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "Value value Value", 3 | "object": {"string_value": "Value-value |Value|"}, 4 | "list_of_string_values": ["value1", {"key": "123"}, "value2"] 5 | } -------------------------------------------------------------------------------- /t/test_suite/positive/09-integer/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "integer", 3 | "number2": ["integer"], 4 | "number3": [ { "integer": [] } ], 5 | "empty_field": "integer", 6 | "missed_field": "integer" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/13-max_number/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "max_number": 10 }, 3 | "number2": { "max_number": [20] }, 4 | "number3": [ { "max_number": [30] } ], 5 | "empty_field": { "max_number": 10 } 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/14-min_number/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "min_number": 10 }, 3 | "number2": { "min_number": [20] }, 4 | "number3": [ { "min_number": [30] } ], 5 | "empty_field": { "min_number": 10 } 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/08-like/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Ivanovich", 5 | "age": 35, 6 | "empty_name": "", 7 | "extra_name": "extra_name" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/10-positive_integer/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "positive_integer", 3 | "number2": ["positive_integer"], 4 | "number3": [ { "positive_integer": [] } ], 5 | "empty_field": "positive_integer" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/34-leave_only/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "Value value Value", 3 | "object": {"string_value": "Value-value |Value|"}, 4 | "list_of_string_values": ["value1", {"key": "123"}, "value123"] 5 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/03-adult_age_in_user/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "name": "REQUIRED", 4 | "age1": "TOO_LOW", 5 | "age2": "WRONG_AGE" 6 | }, 7 | "user_custom_error": "WRONG_USER" 8 | } -------------------------------------------------------------------------------- /t/test_suite/positive/05-max_length/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Васек", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": "1111", 6 | "decimal": "1.2", 7 | "empty_name": "" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/12-positive_decimal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10", 3 | "number2": "120.11", 4 | "number3": "99999.9999", 5 | "number4": "0.01", 6 | "empty_field": "", 7 | "extra_field": "100" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/19-list_of/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": [ 10, 20, 100, 10 ], 3 | "product_ids2": [ 10, 20, 100, 10 ], 4 | "product_ids3": [ 10, 20, 100, 10 ], 5 | "user_ids": "", 6 | "empty_list": [] 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/19-list_of/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": [ 10, 20, 100, 10 ], 3 | "product_ids2": [ 10, 20, 100, 10 ], 4 | "product_ids3": [ 10, 20, 100, 10 ], 5 | "user_ids": "", 6 | "empty_list": [] 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/23-url/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "https://google.com", 3 | "url2": "HTTP://127.0.0.1:3233/?param_1=123¶m_2=asd", 4 | "url3": "http://webbylab.com/?param_1=123#anchor", 5 | "empty_field": "" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/25-eq/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "city3": "Kiev", 5 | "city4": "Dnipro", 6 | "number1": 2, 7 | "number2": "2", 8 | "empty_city": "" 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/negative/18-nested_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "Russia", 4 | "zip": "AAA", 5 | "street": "", 6 | "building": "-10" 7 | }, 8 | "address2": "Should be hash" 9 | } -------------------------------------------------------------------------------- /t/test_suite/negative/27-any_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_string": "any_object", 3 | "value_is_number": "any_object", 4 | "value_is_array": "any_object", 5 | "value_is_empty_array": "any_object" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/04-min_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": 1111, 6 | "empty_name": "", 7 | "extra_name": "extra_name" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/27-any_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "object1": "any_object", 3 | "object2": [{ "any_object": [] }], 4 | "object3": "any_object", 5 | "empty_field": "any_object", 6 | "missed_field": "any_object" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/negative/26-string/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_hash": "FORMAT_ERROR", 3 | "value_is_empty_hash": "FORMAT_ERROR", 4 | "value_is_array": "FORMAT_ERROR", 5 | "value_is_empty_array": "FORMAT_ERROR" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/06-length_equal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": 1111, 6 | "empty_name": "", 7 | "extra_name": "extra_name" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/07-length_between/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "nick_name": "joney_k", 6 | "number1": "1111", 7 | "empty_name": "" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/negative/27-any_object/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_is_string": "FORMAT_ERROR", 3 | "value_is_number": "FORMAT_ERROR", 4 | "value_is_array": "FORMAT_ERROR", 5 | "value_is_empty_array": "FORMAT_ERROR" 6 | } 7 | -------------------------------------------------------------------------------- /t/test_suite/positive/33-remove/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": {"remove": "V "}, 3 | "object": {"nested_object": { 4 | "string_value": {"remove": "a-zv|" } 5 | }}, 6 | "list_of_string_values": {"list_of": {"remove": "2"} } 7 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/02-address/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "street": "REQUIRED", 4 | "zip": "NOT_POSITIVE_INTEGER", 5 | "city": "NOT_ALLOWED_VALUE" 6 | }, 7 | "address_custom_error": "WRONG_ADDRESS" 8 | } -------------------------------------------------------------------------------- /t/test_suite/negative/22-not_empty_list/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "list1": "CANNOT_BE_EMPTY", 3 | "list2": "CANNOT_BE_EMPTY", 4 | "not_list": "FORMAT_ERROR", 5 | "empty_field": "CANNOT_BE_EMPTY", 6 | "missed_field": "CANNOT_BE_EMPTY" 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/24-iso_date/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "2014-02-18", 3 | "iso_date2": "1930-03-30", 4 | "iso_date3": "1930-01-01", 5 | "iso_date4": "1970-01-01", 6 | "iso_date5": "2015-10-10", 7 | "empty_field": "" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/negative/22-not_empty_list/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "list1": "not_empty_list", 3 | "list2": [{ "not_empty_list" : [] }], 4 | "not_list": "not_empty_list", 5 | "empty_field": "not_empty_list", 6 | "missed_field": ["not_empty_list"] 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/03-one_of/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "city3": "Kiev", 5 | "number1": 2, 6 | "number2": "2", 7 | "number3": 1.2, 8 | "boolean": true, 9 | "empty_city": "" 10 | } 11 | -------------------------------------------------------------------------------- /t/test_suite/positive/17-equal_to_field/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": { "equal_to_field": "field2" }, 3 | "field2": { "equal_to_field": ["field1"] }, 4 | "field3": { "equal_to_field": ["field1"] }, 5 | "empty_field": { "equal_to_field": "field2" } 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/23-url/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "https://google.com", 3 | "url2": "HTTP://127.0.0.1:3233/?param_1=123¶m_2=asd", 4 | "url3": "http://webbylab.com/?param_1=123#anchor", 5 | "empty_field": "", 6 | "extra_field": "aaaa" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/12-positive_decimal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "positive_decimal", 3 | "number2": ["positive_decimal"], 4 | "number3": [ { "positive_decimal": [] } ], 5 | "number4": "positive_decimal", 6 | "empty_field": "positive_decimal" 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/25-eq/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "city3": "Kiev", 5 | "city4": "Dnipro", 6 | "number1": "2", 7 | "number2": 2, 8 | "empty_city": "", 9 | "extra_city": "New York" 10 | } 11 | -------------------------------------------------------------------------------- /t/test_suite/positive/30-trim/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "trim", 3 | "decimal_value": ["trim"], 4 | "unicode_string": "trim", 5 | "object": {"nested_object": {"string_value": "trim"} }, 6 | "list_of_string_values": {"list_of": "trim"} 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/32-to_uc/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "Value", 3 | "unicode_string":"привет", 4 | "decimal_value": 1.2, 5 | "object": {"string_value": "Some Value"}, 6 | "list_of_string_values": ["value1", {"key": "123"}, "value2"] 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/34-leave_only/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": {"leave_only": "Vl "}, 3 | "object": {"nested_object": { 4 | "string_value": {"leave_only": "a-zv" } 5 | }}, 6 | "list_of_string_values": {"list_of": {"leave_only": "23"} } 7 | } -------------------------------------------------------------------------------- /lib/rules/special/url.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'url' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | url: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/05-max_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Васек", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "number1": 1111, 6 | "decimal": 1.2, 7 | "empty_name": "", 8 | "extra_name": "extra_name" 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/positive/30-trim/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "value", 3 | "decimal_value": "1.2", 4 | "unicode_string": "Привет", 5 | "object": {"string_value": "some value"}, 6 | "list_of_string_values": ["value1", {"key": "123"}, "value2"] 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/32-to_uc/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "VALUE", 3 | "unicode_string": "ПРИВЕТ", 4 | "decimal_value": "1.2", 5 | "object": {"string_value": "SOME VALUE"}, 6 | "list_of_string_values": ["VALUE1", {"key": "123"}, "VALUE2"] 7 | } 8 | -------------------------------------------------------------------------------- /t/test_suite/positive/32-to_uc/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "to_uc", 3 | "unicode_string": "to_uc", 4 | "decimal_value": ["to_uc"], 5 | "object": {"nested_object": {"string_value": "to_uc"} }, 6 | "list_of_string_values": {"list_of": "to_uc"} 7 | } 8 | -------------------------------------------------------------------------------- /lib/rules/modifiers/trim.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'trim' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | trim: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/string/like.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'like' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | like: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/30-trim/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": " value ", 3 | "decimal_value": 1.2, 4 | "unicode_string": " Привет ", 5 | "object": {"string_value": " some value "}, 6 | "list_of_string_values": [" value1 ", {"key": "123"}, " value2 "] 7 | } 8 | -------------------------------------------------------------------------------- /lib/rules/special/email.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'email' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | email: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/string/string.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'string' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | string: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/01-adult_age/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "age1": 32, 3 | "age2": 33, 4 | "age3": 35, 5 | "age4": 36, 6 | "age1_custom_error": 37, 7 | "age2_custom_error": 38, 8 | "age3_custom_error": 100, 9 | "age4_custom_error": 110 10 | } -------------------------------------------------------------------------------- /t/test_suite/positive/07-length_between/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "nick_name": "joney_k", 6 | "number1": 1111, 7 | "empty_name": "", 8 | "extra_name": "extra_name" 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/positive/24-iso_date/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "iso_date", 3 | "iso_date2": { "iso_date": [] }, 4 | "iso_date3": [ { "iso_date": [] } ], 5 | "iso_date4": [ { "iso_date": [] } ], 6 | "iso_date5": "iso_date", 7 | "empty_field": "iso_date" 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/common/required.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'required' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | required: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/modifiers/remove.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'remove' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | remove: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/numeric/decimal.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'decimal' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | decimal: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/numeric/integer.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'integer' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | integer: RuleTypeDef; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/negative/18-nested_object/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "NOT_ALLOWED_VALUE", 4 | "zip": "NOT_POSITIVE_INTEGER", 5 | "street": "REQUIRED", 6 | "building": "NOT_POSITIVE_INTEGER" 7 | }, 8 | "address2": "FORMAT_ERROR" 9 | } -------------------------------------------------------------------------------- /lib/rules/common/not_empty.js: -------------------------------------------------------------------------------- 1 | function not_empty() { 2 | return value => { 3 | if (value !== null && value !== undefined && value === '') { 4 | return 'CANNOT_BE_EMPTY'; 5 | } 6 | 7 | return; 8 | }; 9 | } 10 | 11 | module.exports = not_empty; -------------------------------------------------------------------------------- /t/test_suite/positive/03-one_of/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "Moscow", 3 | "city2": "Kiev", 4 | "city3": "Kiev", 5 | "number1": 2, 6 | "number2": 2, 7 | "number3": 1.2, 8 | "boolean": true, 9 | "empty_city": "", 10 | "extra_city": "New York" 11 | } 12 | -------------------------------------------------------------------------------- /t/test_suite/positive/24-iso_date/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "2014-02-18", 3 | "iso_date2": "1930-03-30", 4 | "iso_date3": "1930-01-01", 5 | "iso_date4": "1970-01-01", 6 | "iso_date5": "2015-10-10", 7 | "empty_field": "", 8 | "extra_field": "aaaa" 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/common/required.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function required() { 4 | return value => { 5 | if (util.isNoValue(value)) { 6 | return 'REQUIRED'; 7 | } 8 | 9 | return; 10 | }; 11 | }; 12 | 13 | module.exports = required; -------------------------------------------------------------------------------- /lib/rules/modifiers/default.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | module.exports = defaultValue => { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) { 6 | outputArr.push(defaultValue); 7 | } 8 | }; 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /t/test_suite/negative/01-required/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "", 3 | "last_name": null, 4 | "age": "25", 5 | 6 | "value_is_hash": {"test": 1}, 7 | "value_is_empty_hash": {}, 8 | "value_is_array": ["test", 1], 9 | "value_is_empty_array": [] 10 | } -------------------------------------------------------------------------------- /t/test_suite/negative/02-not_empty/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "", 3 | "last_name": null, 4 | "age": "25", 5 | 6 | "value_is_hash": {"test": 1}, 7 | "value_is_empty_hash": {}, 8 | "value_is_array": ["test", 1], 9 | "value_is_empty_array": [] 10 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/01-adult_age/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "age1": 32, 3 | "age2": 33, 4 | "age3": 15, 5 | "age4": -20, 6 | "age1_custom_error": 37, 7 | "age2_custom_error": 38, 8 | "age3_custom_error": 16, 9 | "age4_custom_error": -10, 10 | "extra_field": 12 11 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/01-adult_age/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "age1": 32, 3 | "age2": 33, 4 | "age3": 35, 5 | "age4": 36, 6 | "age1_custom_error": 37, 7 | "age2_custom_error": 38, 8 | "age3_custom_error": 100, 9 | "age4_custom_error": 110, 10 | "extra_field": 12 11 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/03-adult_age_in_user/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "name": "koorchik", 4 | "age1": 20, 5 | "age2": 40 6 | }, 7 | "user_custom_error": { 8 | "name": "koorchik", 9 | "age1": 33, 10 | "age2": 22 11 | } 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/20-list_of_objects/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ "required", { "list_of_objects": { 3 | "product_id": [ "required","positive_integer" ], 4 | "quantity": [ "required", "positive_integer" ] 5 | }}], 6 | "users": {"list_of_objects" : {"name": "required"} } 7 | } -------------------------------------------------------------------------------- /t/test_suite/positive/18-nested_object/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "Ukraine", 4 | "zip": 12345, 5 | "street": "10", 6 | "building": 10 7 | }, 8 | 9 | "should_not_be_errored": "", 10 | "should_be_empty": { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/positive/25-eq/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": { "eq": "Moscow" }, 3 | "city2": [{ "eq": "Kiev" }], 4 | "city3": { "eq": "Kiev" }, 5 | "city4": [{ "eq": ["Dnipro"] }], 6 | "number1": { "eq": 2 }, 7 | "number2": { "eq": "2" }, 8 | "empty_city": { "eq": "Kiev" } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/positive/31-to_lc/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "Value", 3 | "decimal_value": 1.2, 4 | "unicode_string": "ПРИВЕТ", 5 | "object": {"string_value": "Some Value"}, 6 | "list_of_string_values": ["VALUE1", {"key": "123"}, "VALUE2"], 7 | "to_lc_with_another_rule": "Value" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/negative/19-list_of/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": [ "NOT_POSITIVE_INTEGER", "REQUIRED", null, "TOO_HIGH" ], 3 | "product_ids2": [ "NOT_POSITIVE_INTEGER", "REQUIRED", null, "TOO_HIGH" ], 4 | "product_ids3": [ "NOT_POSITIVE_INTEGER", null, null, null ], 5 | "user_ids": "FORMAT_ERROR" 6 | } -------------------------------------------------------------------------------- /t/test_suite/positive/31-to_lc/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "value", 3 | "decimal_value": "1.2", 4 | "unicode_string": "привет", 5 | "object": {"string_value": "some value"}, 6 | "list_of_string_values": ["value1", {"key": "123"}, "value2"], 7 | "to_lc_with_another_rule": "value" 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/04-min_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "min_length": 3 }, 3 | "last_name": { "min_length": [4] }, 4 | "middle_name": [{ "min_length": [5] }], 5 | "number1": {"min_length": 2}, 6 | "empty_name": { "min_length": 3 }, 7 | "missed_name": { "min_length": 3 } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/15-number_between/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "number_between": [10, 20] }, 3 | "number2": { "number_between": [20, 30] }, 4 | "number3": [ { "number_between": [-1, 1] } ], 5 | "number4": [ { "number_between": [-10, 10] } ], 6 | "empty_field": { "number_between": [10, 20] } 7 | } 8 | -------------------------------------------------------------------------------- /lib/rules/modifiers/to_lc.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'to_lc' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | to_lc: RuleTypeDef; 7 | toLc: RuleTypeRegistry['to_lc']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/modifiers/to_uc.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'to_uc' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | to_uc: RuleTypeDef; 7 | toUc: RuleTypeRegistry['to_uc']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/02-address/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "street": "My street", 4 | "zip": 1232131, 5 | "city": "Kiev" 6 | }, 7 | "address_custom_error": { 8 | "street": "My street", 9 | "zip": 12312, 10 | "city": "Kharkiv" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/23-url/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "url", 3 | "url2": { "url": [] }, 4 | "url3": [ { "url": [] } ], 5 | "empty_field": "url", 6 | 7 | "value_is_hash": "url", 8 | "value_is_empty_hash": "url", 9 | "value_is_array": "url", 10 | "value_is_empty_array": "url" 11 | } -------------------------------------------------------------------------------- /t/test_suite/positive/31-to_lc/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "string_value": "to_lc", 3 | "decimal_value": ["to_lc"], 4 | "unicode_string": "to_lc", 5 | "object": {"nested_object": {"string_value": "to_lc"} }, 6 | "list_of_string_values": {"list_of": "to_lc"}, 7 | "to_lc_with_another_rule": ["to_lc", "required"] 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/common/any_object.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function any_object() { 4 | return value => { 5 | if (util.isNoValue(value)) return; 6 | 7 | if (!util.isObject(value)) { 8 | return 'FORMAT_ERROR'; 9 | } 10 | }; 11 | }; 12 | 13 | module.exports = any_object; -------------------------------------------------------------------------------- /t/test_suite/negative/23-url/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "WRONG_URL", 3 | "url2": "WRONG_URL", 4 | "url3": "WRONG_URL", 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 | } -------------------------------------------------------------------------------- /t/test_suite/positive/35-default/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "empty_value1": "", 3 | "empty_value2": "", 4 | "empty_value3": "", 5 | "zero_value": 0, 6 | "null_value": null, 7 | "string_value": "Value", 8 | "object": {"name": "Value"}, 9 | "list_of_string_values": ["value1", {"key": "123"}, "value2"] 10 | } 11 | -------------------------------------------------------------------------------- /t/test_suite/positive/06-length_equal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "length_equal": 5 }, 3 | "last_name": { "length_equal": [6] }, 4 | "middle_name": [{ "length_equal": [9] }], 5 | "number1": { "length_equal": 4 }, 6 | "empty_name": { "length_equal": 3 }, 7 | "missed_name": { "length_equal": 3 } 8 | } 9 | -------------------------------------------------------------------------------- /t/test_suite/positive/08-like/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "like": "[A-Za-z]+" }, 3 | "last_name": { "like": ["^[A-Za-z]+$"] }, 4 | "middle_name": { "like": ["^[a-z]+$", "i"] }, 5 | "age": [{ "like": "^[0-9]+$" }], 6 | "empty_name": { "like": "[A-Za-z]" }, 7 | "missed_name": { "like": "[A-Za-z]" } 8 | } 9 | -------------------------------------------------------------------------------- /lib/rules/special/iso_date.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'iso_date' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | iso_date: RuleTypeDef; 7 | isoDate: RuleTypeRegistry['iso_date']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/02-address/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "street": "", 4 | "zip": -1232131, 5 | "city": "Moscow" 6 | }, 7 | "address_custom_error": { 8 | "street": "My street", 9 | "zip": "12312", 10 | "city": "Moscow" 11 | }, 12 | "extra_field": 12 13 | } -------------------------------------------------------------------------------- /lib/rules/common/not_empty.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'not_empty' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | not_empty: RuleTypeDef; 7 | notEmpty: RuleTypeRegistry['not_empty']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/02-address/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "street": "My street", 4 | "zip": 1232131, 5 | "city": "Kiev" 6 | }, 7 | "address_custom_error": { 8 | "street": "My street", 9 | "zip": "12312", 10 | "city": "Kharkiv" 11 | }, 12 | "extra_field": 12 13 | } -------------------------------------------------------------------------------- /t/test_suite/negative/01-required/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "required", 3 | "last_name": ["required"], 4 | "middle_name": [ { "required": [] } ], 5 | 6 | "value_is_hash": "required", 7 | "value_is_empty_hash": "required", 8 | "value_is_array": "required", 9 | "value_is_empty_array": "required" 10 | } -------------------------------------------------------------------------------- /lib/rules/modifiers/leave_only.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'leave_only' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | leave_only: RuleTypeDef; 7 | leaveOnly: RuleTypeRegistry['leave_only']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/numeric/max_number.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'max_number' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | max_number: RuleTypeDef; 7 | maxNumber: RuleTypeRegistry['max_number']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/numeric/min_number.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'min_number' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | min_number: RuleTypeDef; 7 | minNumber: RuleTypeRegistry['min_number']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/string/max_length.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'max_length' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | max_length: RuleTypeDef; 7 | maxLength: RuleTypeRegistry['max_length']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/string/min_length.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'min_length' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | min_length: RuleTypeDef; 7 | minLength: RuleTypeRegistry['min_length']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/01-adult_age/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "adult_age", 4 | "rules": ["positive_integer", {"min_number": 18}] 5 | }, 6 | 7 | { 8 | "name": "adult_age_with_custom_error", 9 | "rules": ["positive_integer", {"min_number": 18}], 10 | "error": "WRONG_AGE" 11 | } 12 | ] -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/01-adult_age/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "adult_age", 4 | "rules": ["positive_integer", {"min_number": 18}] 5 | }, 6 | 7 | { 8 | "name": "adult_age_with_custom_error", 9 | "rules": ["positive_integer", {"min_number": 18}], 10 | "error": "WRONG_AGE" 11 | } 12 | ] -------------------------------------------------------------------------------- /t/test_suite/negative/05-max_length/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "TOO_LONG", 3 | "last_name": "TOO_LONG", 4 | "middle_name": "TOO_LONG", 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 | } -------------------------------------------------------------------------------- /t/test_suite/positive/20-list_of_objects/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ "required", { "list_of_objects": { 3 | "product_id": [ "required","positive_integer" ], 4 | "quantity": [ "required", "positive_integer" ] 5 | }}], 6 | 7 | "empty_list": [{"list_of_objects": { 8 | "some_field": ["required"] 9 | }}] 10 | } -------------------------------------------------------------------------------- /t/test_suite/negative/02-not_empty/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "not_empty", 3 | "last_name": ["not_empty"], 4 | "middle_name": [ { "not_empty": [] } ], 5 | 6 | "value_is_hash": "not_empty", 7 | "value_is_empty_hash": "not_empty", 8 | "value_is_array": "not_empty", 9 | "value_is_empty_array": "not_empty" 10 | } -------------------------------------------------------------------------------- /t/test_suite/negative/04-min_length/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "TOO_SHORT", 3 | "last_name": "TOO_SHORT", 4 | "middle_name": "TOO_SHORT", 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 | } -------------------------------------------------------------------------------- /t/test_suite/negative/06-length_equal/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "TOO_LONG", 3 | "last_name": "TOO_SHORT", 4 | "middle_name": "TOO_LONG", 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 | } -------------------------------------------------------------------------------- /t/test_suite/negative/07-length_between/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "TOO_SHORT", 3 | "last_name": "TOO_LONG", 4 | "middle_name": "TOO_LONG", 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 | } -------------------------------------------------------------------------------- /lib/rules/common/not_empty_list.js: -------------------------------------------------------------------------------- 1 | function not_empty_list() { 2 | return list => { 3 | if (list === undefined || list === '') return 'CANNOT_BE_EMPTY'; 4 | if (!Array.isArray(list)) return 'FORMAT_ERROR'; 5 | if (list.length < 1) return 'CANNOT_BE_EMPTY'; 6 | return; 7 | }; 8 | }; 9 | 10 | module.exports = not_empty_list; -------------------------------------------------------------------------------- /lib/rules/string/length_equal.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'length_equal' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | length_equal: RuleTypeDef; 7 | lengthEqual: RuleTypeRegistry['length_equal']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/03-adult_age_in_user/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "name": "", 4 | "age1": 15, 5 | "age2": 15, 6 | "extra_field": "som value" 7 | }, 8 | "user_custom_error": { 9 | "name": "koorchik", 10 | "age1": 15, 11 | "age2": 22 12 | }, 13 | "extra_field": 12 14 | } -------------------------------------------------------------------------------- /lib/rules/common/any_object.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'any_object' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | any_object: RuleTypeDef, false, false>; 7 | anyObject: RuleTypeRegistry['any_object']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/string/eq.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'eq' rule 2 | import type { RuleTypeDef, LIVRPrimitive } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | eq: RuleTypeDef< 7 | (value: T) => T, 8 | false, 9 | false 10 | >; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/03-adult_age_in_user/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "name": "koorchik", 4 | "age1": 20, 5 | "age2": 40, 6 | "extra_field": "som value" 7 | }, 8 | "user_custom_error": { 9 | "name": "koorchik", 10 | "age1": 33, 11 | "age2": 22 12 | }, 13 | "extra_field": 12 14 | } -------------------------------------------------------------------------------- /t/test_suite/negative/13-max_number/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "TOO_HIGH", 3 | "number2": "TOO_HIGH", 4 | 5 | "value_is_string": "NOT_NUMBER", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/17-equal_to_field/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": "FIELDS_NOT_EQUAL", 3 | "field2": "FIELDS_NOT_EQUAL", 4 | "field3": "FIELDS_NOT_EQUAL", 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 | } -------------------------------------------------------------------------------- /lib/rules/common/not_empty_list.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'not_empty_list' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | not_empty_list: RuleTypeDef; 7 | notEmptyList: RuleTypeRegistry['not_empty_list']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/numeric/number_between.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'number_between' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | number_between: RuleTypeDef; 7 | numberBetween: RuleTypeRegistry['number_between']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/special/equal_to_field.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'equal_to_field' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | equal_to_field: RuleTypeDef; 7 | equalToField: RuleTypeRegistry['equal_to_field']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/string/length_between.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'length_between' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | length_between: RuleTypeDef; 7 | lengthBetween: RuleTypeRegistry['length_between']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/string/string.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function string() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | outputArr.push(value + ''); 9 | return; 10 | }; 11 | } 12 | 13 | module.exports = string; -------------------------------------------------------------------------------- /t/test_suite/negative/03-one_of/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "New York", 3 | "city2": "New York", 4 | "city3": "New York", 5 | "number1": "1.0", 6 | "extra_city": "New York", 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 | -------------------------------------------------------------------------------- /t/test_suite/positive/35-default/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "empty_value1": 10, 3 | "empty_value2": [], 4 | "empty_value3": {}, 5 | "zero_value": 0, 6 | "null_value": "12", 7 | "missed_value": "15", 8 | 9 | "string_value": "Value", 10 | "object": {"name": "Value"}, 11 | "list_of_string_values": ["value1", {"key": "123"}, "value2"] 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/20-list_of_objects/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "product_id": "NOT_POSITIVE_INTEGER", 5 | "quantity": "REQUIRED" 6 | }, 7 | null, 8 | { 9 | "product_id": "NOT_POSITIVE_INTEGER" 10 | }, 11 | "FORMAT_ERROR" 12 | ], 13 | "users": "FORMAT_ERROR" 14 | } -------------------------------------------------------------------------------- /lib/rules/numeric/positive_decimal.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'positive_decimal' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | positive_decimal: RuleTypeDef; 7 | positiveDecimal: RuleTypeRegistry['positive_decimal']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/rules/numeric/positive_integer.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'positive_integer' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | positive_integer: RuleTypeDef; 7 | positiveInteger: RuleTypeRegistry['positive_integer']; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/negative/04-min_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "empty_name": "", 6 | "extra_name": "extra_name", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/05-max_length/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "empty_name": "", 6 | "extra_name": "extra_name", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/06-length_equal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "empty_name": "", 6 | "extra_name": "extra_name", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/13-max_number/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "15", 3 | "number2": "25.5", 4 | "empty_field": "", 5 | "extra_field": "100", 6 | 7 | "value_is_string": "some_string", 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/23-url/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "url1": "www.google.com", 3 | "url2": "ftp://facebook.com", 4 | "url3": "http://g_oogle.com", 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 | } -------------------------------------------------------------------------------- /t/test_suite/positive/05-max_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "max_length": 5 }, 3 | "last_name": { "max_length": [10] }, 4 | "middle_name": [{ "max_length": [10] }], 5 | "number1": { "max_length": 5 }, 6 | "decimal": { "max_length": 3 }, 7 | "empty_name": { "max_length": 3 }, 8 | "missed_name": { "max_length": 3 } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/negative/07-length_between/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Some name", 5 | "empty_name": "", 6 | "extra_name": "extra_name", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/17-equal_to_field/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": "some value1", 3 | "field2": "some value", 4 | "field3": "some value", 5 | "empty_field": "", 6 | "extra_field": "some value", 7 | 8 | "value_is_hash": {"test": 1}, 9 | "value_is_empty_hash": {}, 10 | "value_is_array": ["test", 1], 11 | "value_is_empty_array": [] 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/18-nested_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": [ "required", { "nested_object": { 3 | "country": ["required", { "one_of": [["Ukraine", "USA"]] } ], 4 | "zip": "positive_integer", 5 | "street": "required", 6 | "building": ["required", "positive_integer" ] 7 | } } ], 8 | "address2": { "nested_object": { "country": "required" } } 9 | } -------------------------------------------------------------------------------- /lib/rules/modifiers/trim.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function trim() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value) || typeof value === 'object') return; 6 | 7 | const strValue = typeof value === 'string' ? value : String(value); 8 | outputArr.push(strValue.trim()); 9 | }; 10 | } 11 | 12 | module.exports = trim; -------------------------------------------------------------------------------- /t/test_suite/negative/08-like/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "WRONG_FORMAT", 3 | "last_name": "WRONG_FORMAT", 4 | "middle_name": "WRONG_FORMAT", 5 | "age": "WRONG_FORMAT", 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 | } -------------------------------------------------------------------------------- /t/test_suite/negative/14-min_number/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "TOO_LOW", 3 | "number2": "TOO_LOW", 4 | "number3": "TOO_LOW", 5 | 6 | "value_is_string": "NOT_NUMBER", 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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "baseUrl": "." 10 | }, 11 | "include": [ 12 | "types/**/*.d.ts", 13 | "lib/**/*.d.ts", 14 | "t/types-test.ts", 15 | "examples/**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /lib/rules/modifiers/default.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'default' rule 2 | import type { RuleTypeDef } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | default: RuleTypeDef< 7 | (defaultValue: T) => T, 8 | false, 9 | true // Sets defaultEffect - makes field non-optional 10 | >; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/rules/modifiers/to_uc.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function to_uc() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value) || typeof value === 'object') return; 6 | 7 | const strValue = typeof value === 'string' ? value : String(value); 8 | outputArr.push(strValue.toUpperCase()); 9 | }; 10 | } 11 | 12 | module.exports = to_uc; -------------------------------------------------------------------------------- /t/test_suite/negative/08-like/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Vasya", 3 | "last_name": "Pupkin", 4 | "middle_name": "Ivanovich", 5 | "age": "35", 6 | "empty_name": "", 7 | "extra_name": "extra_name", 8 | 9 | 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } -------------------------------------------------------------------------------- /t/test_suite/negative/09-integer/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "NOT_INTEGER", 3 | "number2": "NOT_INTEGER", 4 | "number3": "NOT_INTEGER", 5 | 6 | "value_is_string": "NOT_INTEGER", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/11-decimal/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "NOT_DECIMAL", 3 | "number2": "NOT_DECIMAL", 4 | "number3": "NOT_DECIMAL", 5 | 6 | "value_is_string": "NOT_DECIMAL", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/24-iso_date/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "WRONG_DATE", 3 | "iso_date2": "WRONG_DATE", 4 | "iso_date3": "WRONG_DATE", 5 | "like_iso_date": "WRONG_DATE", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/03-one_of/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "NOT_ALLOWED_VALUE", 3 | "city2": "NOT_ALLOWED_VALUE", 4 | "city3": "NOT_ALLOWED_VALUE", 5 | "number1": "NOT_ALLOWED_VALUE", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/09-integer/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "A", 3 | "number2": "0.12", 4 | "number3": -1.12, 5 | "empty_field": "", 6 | "extra_field": "100", 7 | 8 | "value_is_string": "some_string", 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/14-min_number/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "5", 3 | "number2": "-10", 4 | "number3": 1.2, 5 | "empty_field": "", 6 | "extra_field": "100", 7 | 8 | "value_is_string": "some_string", 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/positive/20-list_of_objects/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "product_id": 10, 5 | "quantity": "10" 6 | }, 7 | { 8 | "product_id": 20, 9 | "quantity": 20 10 | }, 11 | { 12 | "product_id": 30, 13 | "quantity": 30 14 | } 15 | ], 16 | "empty_list": [] 17 | } -------------------------------------------------------------------------------- /lib/rules/modifiers/to_lc.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function to_lc() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value) || typeof value === 'object') return; 6 | 7 | const strValue = typeof value === 'string' ? value : String(value); 8 | outputArr.push(strValue.toLowerCase()); 9 | }; 10 | } 11 | 12 | module.exports = to_lc; 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/10-positive_integer/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "A", 3 | "number2": "-10", 4 | "number3": "0", 5 | "empty_field": "", 6 | "extra_field": "100", 7 | 8 | "value_is_string": "some_string", 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/11-decimal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "A", 3 | "number2": "12.12.12", 4 | "number3": "1,12", 5 | "empty_field": "", 6 | "extra_field": "100", 7 | 8 | "value_is_string": "some_string", 9 | "value_is_hash": {"test": 1}, 10 | "value_is_empty_hash": {}, 11 | "value_is_array": ["test", 1], 12 | "value_is_empty_array": [] 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/positive/20-list_of_objects/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "product_id": 10, 5 | "quantity": 10 6 | }, 7 | { 8 | "product_id": 20, 9 | "quantity": 20 10 | }, 11 | { 12 | "product_id": 30, 13 | "quantity": 30 14 | } 15 | ], 16 | "empty_list": [] 17 | } 18 | -------------------------------------------------------------------------------- /t/test_suite/negative/09-integer/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "integer", 3 | "number2": ["integer"], 4 | "number3": [ { "integer": [] } ], 5 | "empty_field": "integer", 6 | 7 | "value_is_string": "integer", 8 | "value_is_hash": "integer", 9 | "value_is_empty_hash": "integer", 10 | "value_is_array": "integer", 11 | "value_is_empty_array": "integer" 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/11-decimal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "decimal", 3 | "number2": ["decimal"], 4 | "number3": [ { "decimal": [] } ], 5 | "empty_field": "decimal", 6 | 7 | "value_is_string": "decimal", 8 | "value_is_hash": "decimal", 9 | "value_is_empty_hash": "decimal", 10 | "value_is_array": "decimal", 11 | "value_is_empty_array": "decimal" 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/29-or/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": "NOT_POSITIVE_INTEGER", 3 | "id2-1": "NOT_POSITIVE_INTEGER", 4 | "id3-1": "WRONG_EMAIL", 5 | "id4-1": "CANNOT_BE_EMPTY", 6 | 7 | "products": [ 8 | { 9 | "name": "REQUIRED", 10 | "product_type": "NOT_ALLOWED_VALUE" 11 | }, 12 | { 13 | "name": "REQUIRED" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /lib/rules/special/equal_to_field.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function equal_to_field(field) { 4 | return (value, params) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | if (value != params[field]) return 'FIELDS_NOT_EQUAL'; 9 | return; 10 | }; 11 | } 12 | 13 | module.exports = equal_to_field; -------------------------------------------------------------------------------- /t/test_suite/negative/15-number_beetween/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "TOO_LOW", 3 | "number2": "TOO_HIGH", 4 | "number3": "TOO_LOW", 5 | "number4": "TOO_HIGH", 6 | 7 | "value_is_string": "NOT_NUMBER", 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 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/24-iso_date/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "2014-13-10", 3 | "iso_date2": "2011-02-29", 4 | "iso_date3": "2014-10-10T22:22", 5 | "like_iso_date":"aaaa-bb-cc", 6 | "empty_field": "", 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 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/15-number_beetween/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "5", 3 | "number2": "40", 4 | "number3": -40, 5 | "number4": 40, 6 | "empty_field": "", 7 | "extra_field": "100", 8 | 9 | "value_is_string": "some_string", 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } 15 | -------------------------------------------------------------------------------- /lib/rules/string/one_of.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'one_of' rule 2 | import type { RuleTypeDef, LIVRPrimitive } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | one_of: RuleTypeDef< 7 | (values: T) => T[number], 8 | false, 9 | false 10 | >; 11 | oneOf: RuleTypeRegistry['one_of']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/24-iso_date/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_date1": "iso_date", 3 | "iso_date2": { "iso_date": [] }, 4 | "iso_date3": [ { "iso_date": [] } ], 5 | "like_iso_date": [ { "iso_date": [] } ], 6 | "empty_field": "iso_date", 7 | 8 | "value_is_hash": "iso_date", 9 | "value_is_empty_hash": "iso_date", 10 | "value_is_array": "iso_date", 11 | "value_is_empty_array": "iso_date" 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/positive/35-default/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "empty_value1": {"default": [10]}, 3 | "empty_value2": {"default": [[]]}, 4 | "empty_value3": [{"default": {}}], 5 | "zero_value": {"default": 10}, 6 | "null_value": {"default":"12"}, 7 | "missed_value": {"default": "15"}, 8 | 9 | "string_value": {"default": 10}, 10 | "object": {"default": 10}, 11 | "list_of_string_values": {"default": 10} 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/10-positive_integer/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "NOT_POSITIVE_INTEGER", 3 | "number2": "NOT_POSITIVE_INTEGER", 4 | "number3": "NOT_POSITIVE_INTEGER", 5 | 6 | "value_is_string": "NOT_POSITIVE_INTEGER", 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 | -------------------------------------------------------------------------------- /t/test_suite/negative/12-positive_decimal/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "10.10.10", 3 | "number2": "0", 4 | "number3": -1.12, 5 | "number4": "0.0", 6 | "empty_field": "", 7 | "extra_field": "100", 8 | 9 | "value_is_string": "some_string", 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } 15 | -------------------------------------------------------------------------------- /t/test_suite/negative/20-list_of_objects/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "product_id": 0, 5 | "quantity": "" 6 | }, 7 | { 8 | "product_id": 100, 9 | "quantity": 10 10 | }, 11 | { 12 | "product_id": -10, 13 | "quantity": 10 14 | }, 15 | "Some Wrong Data" 16 | ], 17 | "users": "not an array" 18 | } -------------------------------------------------------------------------------- /t/test_suite/positive/03-one_of/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": { "one_of": [["Moscow", "Kiev" ]] }, 3 | "city2": [{ "one_of": [["Moscow", "Kiev" ]] }], 4 | "city3": { "one_of": ["Moscow", "Kiev"] }, 5 | "number1": { "one_of": [1, 2] }, 6 | "number2": { "one_of": ["1", "2"] }, 7 | "number3": { "one_of": 1.2 }, 8 | "boolean": { "one_of": [true, false] }, 9 | "empty_city": { "one_of": [["Moscow", "Kiev" ]] } 10 | } 11 | -------------------------------------------------------------------------------- /t/test_suite/positive/07-length_between/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "length_between": [4, 6] }, 3 | "last_name": { "length_between": [6, 6] }, 4 | "middle_name": [{ "length_between": [0, 100] }], 5 | "nick_name": [{ "length_between": [0, 100] }], 6 | "number1": { "length_between": [2, 4] }, 7 | "empty_name": { "length_between": [2, 3] }, 8 | "missed_name": { "length_between": [2, 3] } 9 | } 10 | -------------------------------------------------------------------------------- /t/test_suite/positive/18-nested_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "Ukraine", 4 | "zip": "12345", 5 | "street": "10", 6 | "building": "10", 7 | "extra_field": "will be removed" 8 | }, 9 | 10 | "should_not_be_errored": "", 11 | 12 | "should_be_empty": { 13 | "extra_field": "will be removed" 14 | }, 15 | 16 | "extra_field": "will be removed" 17 | } 18 | -------------------------------------------------------------------------------- /t/test_suite/negative/28-variable_object/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "NOT_POSITIVE_INTEGER", 3 | "product1": { 4 | "material_id": "NOT_POSITIVE_INTEGER", 5 | "quantity": "REQUIRED" 6 | }, 7 | "product2": { 8 | "warehouse_id": "NOT_POSITIVE_INTEGER" 9 | }, 10 | "product3": { 11 | "name": "TOO_LONG" 12 | }, 13 | "product4": "FORMAT_ERROR", 14 | "product5": "FORMAT_ERROR" 15 | } 16 | -------------------------------------------------------------------------------- /lib/rules/string/max_length.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function max_length(maxLength) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | value += ''; 9 | if (value.length > maxLength) return 'TOO_LONG'; 10 | outputArr.push(value); 11 | }; 12 | } 13 | 14 | module.exports = max_length; -------------------------------------------------------------------------------- /lib/rules/string/min_length.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function min_length(minLength) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | value += ''; 9 | if (value.length < minLength) return 'TOO_SHORT'; 10 | outputArr.push(value); 11 | }; 12 | } 13 | 14 | module.exports = min_length; -------------------------------------------------------------------------------- /t/test_suite/negative/04-min_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "min_length": 6 }, 3 | "last_name": { "min_length": [10] }, 4 | "middle_name": [{ "min_length": [10] }], 5 | "empty_name": { "min_length": 3 }, 6 | 7 | "value_is_hash": { "min_length": 6 }, 8 | "value_is_empty_hash": { "min_length": 6 }, 9 | "value_is_array": { "min_length": 6 }, 10 | "value_is_empty_array": { "min_length": 6 } 11 | } -------------------------------------------------------------------------------- /t/test_suite/negative/05-max_length/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "max_length": 4 }, 3 | "last_name": { "max_length": [5] }, 4 | "middle_name": [{ "max_length": [0] }], 5 | "empty_name": { "max_length": 3 }, 6 | 7 | "value_is_hash": { "max_length": 4 }, 8 | "value_is_empty_hash": { "max_length": 4 }, 9 | "value_is_array": { "max_length": 4 }, 10 | "value_is_empty_array": { "max_length": 4 } 11 | } -------------------------------------------------------------------------------- /t/test_suite/negative/25-eq/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "New York", 3 | "city2": "New York", 4 | "city3": "New York", 5 | "number1": "1.0", 6 | "number2": "asdasd", 7 | "number3": " _", 8 | "empty_string": " ", 9 | "extra_city": "New York", 10 | "value_is_hash": {"test": 1}, 11 | "value_is_empty_hash": {}, 12 | "value_is_array": ["test", 1], 13 | "value_is_empty_array": [] 14 | } 15 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/01-adult_age/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "age1": "adult_age", 3 | "age2": ["adult_age"], 4 | "age3": [ { "adult_age": [] } ], 5 | "age4": { "adult_age": [] }, 6 | "age1_custom_error": "adult_age_with_custom_error", 7 | "age2_custom_error": ["adult_age_with_custom_error"], 8 | "age3_custom_error": [ { "adult_age_with_custom_error": [] } ], 9 | "age4_custom_error": { "adult_age_with_custom_error": [] } 10 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/01-adult_age/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "age1": "adult_age", 3 | "age2": ["adult_age"], 4 | "age3": [ { "adult_age": [] } ], 5 | "age4": { "adult_age": [] }, 6 | "age1_custom_error": "adult_age_with_custom_error", 7 | "age2_custom_error": ["adult_age_with_custom_error"], 8 | "age3_custom_error": [ { "adult_age_with_custom_error": [] } ], 9 | "age4_custom_error": { "adult_age_with_custom_error": [] } 10 | } -------------------------------------------------------------------------------- /t/test_suite/negative/13-max_number/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "max_number": 10 }, 3 | "number2": [{ "max_number": 20 }], 4 | "empty_field": { "max_number": 40 }, 5 | 6 | "value_is_string": { "max_number": 10 }, 7 | "value_is_hash": { "max_number": 10 }, 8 | "value_is_empty_hash": { "max_number": 10 }, 9 | "value_is_array": { "max_number": 10 }, 10 | "value_is_empty_array": { "max_number": 10 } 11 | } 12 | -------------------------------------------------------------------------------- /t/test_suite/negative/06-length_equal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "length_equal": 2 }, 3 | "last_name": { "length_equal": [7] }, 4 | "middle_name": [{ "length_equal": [3] }], 5 | "empty_name": { "length_equal": 3 }, 6 | 7 | "value_is_hash": { "length_equal": 2 }, 8 | "value_is_empty_hash": { "length_equal": 2 }, 9 | "value_is_array": { "length_equal": 2 }, 10 | "value_is_empty_array": { "length_equal": 2 } 11 | } -------------------------------------------------------------------------------- /t/test_suite/negative/21-list_of_different_objects/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "NOT_POSITIVE_INTEGER", 3 | "products": [ 4 | { 5 | "material_id": "NOT_POSITIVE_INTEGER", 6 | "quantity": "REQUIRED" 7 | }, 8 | { 9 | "warehouse_id": "NOT_POSITIVE_INTEGER" 10 | }, 11 | { 12 | "name": "TOO_LONG" 13 | }, 14 | "FORMAT_ERROR", 15 | "FORMAT_ERROR" 16 | ] 17 | } -------------------------------------------------------------------------------- /lib/rules/meta/nested_object.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'nested_object' rule 2 | import type { RuleTypeDef, InferFromSchema, LIVRSchema } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | nested_object: RuleTypeDef< 7 | (schema: S) => InferFromSchema, 8 | false, 9 | false 10 | >; 11 | nestedObject: RuleTypeRegistry['nested_object']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/rules/numeric/integer.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function integer() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_INTEGER'; 8 | 9 | if (!Number.isInteger(+value)) return 'NOT_INTEGER'; 10 | outputArr.push(+value); 11 | }; 12 | } 13 | 14 | module.exports = integer; -------------------------------------------------------------------------------- /t/test_suite/negative/12-positive_decimal/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "NOT_POSITIVE_DECIMAL", 3 | "number2": "NOT_POSITIVE_DECIMAL", 4 | "number3": "NOT_POSITIVE_DECIMAL", 5 | "number4": "NOT_POSITIVE_DECIMAL", 6 | 7 | "value_is_string": "NOT_POSITIVE_DECIMAL", 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 | } 13 | -------------------------------------------------------------------------------- /lib/rules/numeric/max_number.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function max_number(maxNumber) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_NUMBER'; 8 | 9 | if (+value > +maxNumber) return 'TOO_HIGH'; 10 | outputArr.push(+value); 11 | }; 12 | } 13 | 14 | module.exports = max_number; -------------------------------------------------------------------------------- /lib/rules/numeric/min_number.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function min_number(minNumber) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_NUMBER'; 8 | 9 | if (+value < +minNumber) return 'TOO_LOW'; 10 | outputArr.push(+value); 11 | }; 12 | } 13 | 14 | module.exports = min_number; -------------------------------------------------------------------------------- /t/test_suite/negative/19-list_of/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": ["required", { "list_of": [[ 3 | "required", 4 | "positive_integer", 5 | { "max_number": 100 } 6 | ]] }], 7 | "product_ids2": ["required", { "list_of": [ 8 | "required", 9 | "positive_integer", 10 | { "max_number": 100 } 11 | ] }], 12 | "product_ids3": ["required", { "list_of": "positive_integer" } ], 13 | "user_ids": { "list_of": [[ "required" ]] } 14 | } -------------------------------------------------------------------------------- /t/test_suite/positive/28-variable_object/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": 10, 3 | "product1": { 4 | "product_type": "material", 5 | "material_id": 12, 6 | "quantity": 11 7 | }, 8 | "product2": { 9 | "product_type": "material", 10 | "material_id": 100, 11 | "quantity": 10, 12 | "warehouse_id": 100 13 | }, 14 | "product3": { 15 | "product_type": "service", 16 | "name": "service1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of_objects.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'list_of_objects' rule 2 | import type { RuleTypeDef, InferFromSchema, LIVRSchema } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | list_of_objects: RuleTypeDef< 7 | (schema: S) => Array>, 8 | false, 9 | false 10 | >; 11 | listOfObjects: RuleTypeRegistry['list_of_objects']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'list_of' rule 2 | import type { RuleTypeDef, InferRuleType, LIVRRuleDefinition } from '../../../types/inference'; 3 | 4 | declare module '../../../types/inference' { 5 | interface RuleTypeRegistry { 6 | list_of: RuleTypeDef< 7 | (rules: R) => Array>, 8 | false, 9 | false 10 | >; 11 | listOf: RuleTypeRegistry['list_of']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/10-positive_integer/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "positive_integer", 3 | "number2": ["positive_integer"], 4 | "number3": [ { "positive_integer": [] } ], 5 | "empty_field": "positive_integer", 6 | 7 | "value_is_string": "positive_integer", 8 | "value_is_hash": "positive_integer", 9 | "value_is_empty_hash": "positive_integer", 10 | "value_is_array": "positive_integer", 11 | "value_is_empty_array": "positive_integer" 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/25-eq/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": { "eq": "Moscow" }, 3 | "city2": [{ "eq": "Moscow" }], 4 | "city3": { "eq": "Kiev" }, 5 | "number1": { "eq": 1 }, 6 | "number2": { "eq": 1 }, 7 | "number3": { "eq": 1.2 }, 8 | "empty_string": {"eq": 1 }, 9 | "value_is_hash": { "eq": "Moscow" }, 10 | "value_is_empty_hash": { "eq": "Moscow" }, 11 | "value_is_array": { "eq": "Moscow" }, 12 | "value_is_empty_array": { "eq": "Moscow" } 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/29-or/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": "Usermail.com", 3 | "id2-1": "Usermail.com", 4 | "id3-1": "Usermail.com", 5 | "id4-1": "", 6 | 7 | "products": [ 8 | { 9 | "product_type": "material", 10 | "material_id": 123, 11 | "quantity": 10, 12 | "warehouse_id": -321 13 | }, 14 | 15 | { 16 | "product_type": "service", 17 | "name": "", 18 | "extra_field": "some data" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /t/test_suite/negative/08-like/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "like": "[0-9]+" }, 3 | "last_name": { "like": ["^[A-Z]+$"] }, 4 | "middle_name": { "like": ["^[b-z]+$", "i"] }, 5 | "age": [{ "like": "^[a-z]+$" }], 6 | "empty_name": { "like": "[A-Za-z]" }, 7 | 8 | "value_is_hash": { "like": "[0-9]+" }, 9 | "value_is_empty_hash": { "like": "[0-9]+" }, 10 | "value_is_array": { "like": "[0-9]+" }, 11 | "value_is_empty_array": { "like": "[0-9]+" } 12 | } -------------------------------------------------------------------------------- /t/test_suite/negative/14-min_number/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "min_number" : 10 }, 3 | "number2": [{ "min_number" : 20 }], 4 | "number3": [{ "min_number" : 5 }], 5 | "empty_field": { "min_number" : 10 }, 6 | 7 | "value_is_string": { "min_number": 10 }, 8 | "value_is_hash": { "min_number": 10 }, 9 | "value_is_empty_hash": { "min_number": 10 }, 10 | "value_is_array": { "min_number": 10 }, 11 | "value_is_empty_array": { "min_number": 10 } 12 | } 13 | -------------------------------------------------------------------------------- /t/test_suite/negative/25-eq/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": "NOT_ALLOWED_VALUE", 3 | "city2": "NOT_ALLOWED_VALUE", 4 | "city3": "NOT_ALLOWED_VALUE", 5 | "number1": "NOT_ALLOWED_VALUE", 6 | "number2": "NOT_ALLOWED_VALUE", 7 | "number3": "NOT_ALLOWED_VALUE", 8 | "empty_string": "NOT_ALLOWED_VALUE", 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 | -------------------------------------------------------------------------------- /lib/rules/string/length_equal.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function length_equal(length) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | value += ''; 9 | if (value.length < length) return 'TOO_SHORT'; 10 | if (value.length > length) return 'TOO_LONG'; 11 | outputArr.push(value); 12 | }; 13 | } 14 | 15 | module.exports = length_equal; -------------------------------------------------------------------------------- /t/test_suite/negative/07-length_between/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": { "length_between": [7, 10]}, 3 | "last_name": { "length_between": [4, 5] }, 4 | "middle_name": [{ "length_between": [2, 3] }], 5 | "empty_name": { "length_between": [4, 10] }, 6 | 7 | "value_is_hash": { "length_between": [7, 10]}, 8 | "value_is_empty_hash": { "length_between": [7, 10]}, 9 | "value_is_array": { "length_between": [7, 10]}, 10 | "value_is_empty_array": { "length_between": [7, 10]} 11 | } -------------------------------------------------------------------------------- /t/test_suite/negative/17-equal_to_field/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": { "equal_to_field": "field2" }, 3 | "field2": { "equal_to_field": ["field1"] }, 4 | "field3": { "equal_to_field": ["field1"] }, 5 | "empty_field": { "equal_to_field": "field2" }, 6 | 7 | "value_is_hash": { "equal_to_field": "field2" }, 8 | "value_is_empty_hash": { "equal_to_field": "field2" }, 9 | "value_is_array": { "equal_to_field": "field2" }, 10 | "value_is_empty_array": { "equal_to_field": "field2" } 11 | } -------------------------------------------------------------------------------- /lib/rules/modifiers/remove.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function remove(chars) { 4 | const escapedChars = util.escapeRegExp(chars); 5 | const re = new RegExp(`[${escapedChars}]`, 'g'); 6 | 7 | return (value, params, outputArr) => { 8 | if (util.isNoValue(value) || typeof value === 'object') return; 9 | 10 | const strValue = typeof value === 'string' ? value : String(value); 11 | outputArr.push(strValue.replace(re, '')); 12 | }; 13 | } 14 | 15 | module.exports = remove; -------------------------------------------------------------------------------- /lib/rules/string/length_between.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function length_between(minLength, maxLength) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | 8 | value += ''; 9 | if (value.length < minLength) return 'TOO_SHORT'; 10 | if (value.length > maxLength) return 'TOO_LONG'; 11 | outputArr.push(value); 12 | }; 13 | } 14 | 15 | module.exports = length_between; -------------------------------------------------------------------------------- /lib/rules/modifiers/leave_only.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function leave_only(chars) { 4 | const escapedChars = util.escapeRegExp(chars); 5 | const re = new RegExp(`[^${escapedChars}]`, 'g'); 6 | 7 | return (value, params, outputArr) => { 8 | if (util.isNoValue(value) || typeof value === 'object') return; 9 | 10 | const strValue = typeof value === 'string' ? value : String(value); 11 | outputArr.push(strValue.replace(re, '')); 12 | }; 13 | } 14 | 15 | module.exports = leave_only; -------------------------------------------------------------------------------- /lib/rules/numeric/positive_decimal.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function positive_decimal() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_POSITIVE_DECIMAL'; 8 | 9 | if (Number.isNaN(+value) || +value <= 0) return 'NOT_POSITIVE_DECIMAL'; 10 | outputArr.push(+value); 11 | }; 12 | } 13 | 14 | module.exports = positive_decimal; 15 | -------------------------------------------------------------------------------- /lib/rules/numeric/positive_integer.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function positive_integer() { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_POSITIVE_INTEGER'; 8 | 9 | if (!Number.isInteger(+value) || +value < 1) return 'NOT_POSITIVE_INTEGER'; 10 | outputArr.push(+value); 11 | }; 12 | } 13 | 14 | module.exports = positive_integer; -------------------------------------------------------------------------------- /t/test_suite/negative/03-one_of/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "city1": { "one_of": [["Moscow", "Kiev" ]] }, 3 | "city2": [{ "one_of": [["Moscow", "Kiev" ]] }], 4 | "city3": { "one_of": ["Moscow", "Kiev"] }, 5 | "number1": { "one_of": [1, "1.00", "1"] }, 6 | 7 | "value_is_hash": { "one_of": [["Moscow", "Kiev" ]] }, 8 | "value_is_empty_hash": { "one_of": [["Moscow", "Kiev" ]] }, 9 | "value_is_array": { "one_of": [["Moscow", "Kiev" ]] }, 10 | "value_is_empty_array": { "one_of": [["Moscow", "Kiev" ]] } 11 | } 12 | -------------------------------------------------------------------------------- /t/test_suite/negative/12-positive_decimal/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": "positive_decimal", 3 | "number2": ["positive_decimal"], 4 | "number3": [ { "positive_decimal": [] } ], 5 | "number4": ["positive_decimal"], 6 | "empty_field": "positive_decimal", 7 | 8 | "value_is_string": "positive_decimal", 9 | "value_is_hash": "positive_decimal", 10 | "value_is_empty_hash": "positive_decimal", 11 | "value_is_array": "positive_decimal", 12 | "value_is_empty_array": "positive_decimal" 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/positive/19-list_of/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_ids1": ["required", { "list_of": [[ 3 | "required", 4 | "positive_integer", 5 | { "max_number": 100 } 6 | ]] }], 7 | "product_ids2": ["required", { "list_of": [ 8 | "required", 9 | "positive_integer", 10 | { "max_number": 100 } 11 | ] }], 12 | "product_ids3": ["required", { "list_of": "positive_integer" } ], 13 | "user_ids": { "list_of": [[ "required" ]] }, 14 | "empty_list": { "list_of": ["required"] } 15 | } -------------------------------------------------------------------------------- /t/test_suite/positive/28-variable_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "10", 3 | "product1": { 4 | "product_type": "material", 5 | "material_id": 12, 6 | "quantity": "11" 7 | }, 8 | "product2": { 9 | "product_type": "material", 10 | "material_id": 100, 11 | "quantity": 10, 12 | "warehouse_id": 100 13 | }, 14 | "product3": { 15 | "product_type": "service", 16 | "name": "service1", 17 | "extra_field": "will be removed" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/rules/numeric/decimal.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | const DECIMAL_RE = /^-?(?:(?:[0-9]+\.[0-9]+)|(?:[0-9]+))$/; 4 | 5 | function decimal() { 6 | return (value, params, outputArr) => { 7 | if (util.isNoValue(value)) return; 8 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 9 | if (!util.looksLikeNumber(value)) return 'NOT_DECIMAL'; 10 | 11 | if (!DECIMAL_RE.test(value + '')) return 'NOT_DECIMAL'; 12 | outputArr.push(+value); 13 | }; 14 | } 15 | 16 | module.exports = decimal; -------------------------------------------------------------------------------- /t/test_suite/positive/29-or/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": "User@mail.com", 3 | "id1-2": 123, 4 | 5 | "id2-1": "user@mail.com", 6 | "id2-2": "123", 7 | 8 | "id3-1": "user@mail.com", 9 | "id3-2": "UserUserUser@mail.com", 10 | 11 | "products": [ 12 | { 13 | "product_type": "material", 14 | "material_id": 123, 15 | "quantity": 10, 16 | "warehouse_id": 321 17 | }, 18 | 19 | { 20 | "product_type": "service", 21 | "name": "Some service" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /lib/rules/string/eq.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function eq(allowedValue) { 4 | const strAllowedValue = String(allowedValue); 5 | 6 | return (value, params, outputArr) => { 7 | if (util.isNoValue(value)) return; 8 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 9 | 10 | if (String(value) === strAllowedValue) { 11 | outputArr.push(allowedValue); 12 | return; 13 | } 14 | 15 | return 'NOT_ALLOWED_VALUE'; 16 | }; 17 | } 18 | 19 | module.exports = eq; 20 | -------------------------------------------------------------------------------- /lib/rules/numeric/number_between.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function number_between(minNumber, maxNumber) { 4 | return (value, params, outputArr) => { 5 | if (util.isNoValue(value)) return; 6 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 7 | if (!util.looksLikeNumber(value)) return 'NOT_NUMBER'; 8 | 9 | if (+value < +minNumber) return 'TOO_LOW'; 10 | if (+value > +maxNumber) return 'TOO_HIGH'; 11 | outputArr.push(+value); 12 | }; 13 | } 14 | 15 | module.exports = number_between; -------------------------------------------------------------------------------- /lib/rules/string/like.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function like(reStr, flags) { 4 | const isIgnoreCase = arguments.length === 3 && flags.match('i'); 5 | const re = new RegExp(reStr, isIgnoreCase ? 'i' : ''); 6 | 7 | return (value, params, outputArr) => { 8 | if (util.isNoValue(value)) return; 9 | 10 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 11 | 12 | value += ''; 13 | if (!value.match(re)) return 'WRONG_FORMAT'; 14 | outputArr.push(value); 15 | }; 16 | } 17 | 18 | module.exports = like; -------------------------------------------------------------------------------- /t/test_suite/positive/21-list_of_different_objects/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": 10, 3 | "products": [ 4 | { 5 | "product_type": "material", 6 | "material_id": 12, 7 | "quantity": 11 8 | }, 9 | { 10 | "product_type": "material", 11 | "material_id": 100, 12 | "quantity": 10, 13 | "warehouse_id": 100 14 | }, 15 | { 16 | "product_type": "service", 17 | "name": "service1" 18 | } 19 | ], 20 | "empty_list": [] 21 | } 22 | -------------------------------------------------------------------------------- /t/test_suite/positive/29-or/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": "User@mail.com", 3 | "id1-2": 123, 4 | 5 | "id2-1": "User@mail.com", 6 | "id2-2": 123, 7 | 8 | "id3-1": "User@mail.com", 9 | "id3-2": "UserUserUser@mail.com", 10 | 11 | "products": [ 12 | { 13 | "product_type": "material", 14 | "material_id": 123, 15 | "quantity": 10, 16 | "warehouse_id": 321 17 | }, 18 | 19 | { 20 | "product_type": "service", 21 | "name": "Some service", 22 | "extra_field": "some data" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /t/test_suite/positive/18-nested_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": [ "required", { "nested_object": { 3 | "country": ["required", { "one_of": [["Ukraine", "USA"]] } ], 4 | "zip": "positive_integer", 5 | "street": "required", 6 | "building": ["required", "positive_integer" ] 7 | } } ], 8 | 9 | "should_not_be_errored": { "nested_object": { 10 | "zip": "positive_integer" 11 | }}, 12 | 13 | "should_be_empty": [ "required", { "nested_object": { 14 | "country": [{ "one_of": [["Ukraine", "USA"]] } ], 15 | "zip": "positive_integer" 16 | } } ] 17 | } 18 | -------------------------------------------------------------------------------- /t/test_suite/positive/21-list_of_different_objects/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "10", 3 | "products": [ 4 | { 5 | "product_type": "material", 6 | "material_id": 12, 7 | "quantity": "11" 8 | }, 9 | { 10 | "product_type": "material", 11 | "material_id": 100, 12 | "quantity": 10, 13 | "warehouse_id": 100 14 | }, 15 | { 16 | "product_type": "service", 17 | "name": "service1", 18 | "extra_field": "will be removed" 19 | } 20 | ], 21 | "empty_list": [] 22 | } -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/02-address/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "address", 4 | "rules": { "nested_object":{ 5 | "street": "required", 6 | "zip": ["required", "positive_integer"], 7 | "city": {"one_of": ["Kiev", "Kharkiv"] } 8 | }} 9 | }, 10 | 11 | { 12 | "name": "address_with_custom_error", 13 | "rules": { "nested_object":{ 14 | "street": "required", 15 | "zip": ["required", "positive_integer"], 16 | "city": {"one_of": ["Kiev", "Kharkiv"] } 17 | }}, 18 | "error": "WRONG_ADDRESS" 19 | } 20 | ] -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/02-address/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "address", 4 | "rules": { "nested_object":{ 5 | "street": "required", 6 | "zip": ["required", "positive_integer"], 7 | "city": {"one_of": ["Kiev", "Kharkiv"] } 8 | }} 9 | }, 10 | 11 | { 12 | "name": "address_with_custom_error", 13 | "rules": { "nested_object":{ 14 | "street": "required", 15 | "zip": ["required", "positive_integer"], 16 | "city": {"one_of": ["Kiev", "Kharkiv"] } 17 | }}, 18 | "error": "WRONG_ADDRESS" 19 | } 20 | ] -------------------------------------------------------------------------------- /t/test_suite/negative/15-number_beetween/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "number1": { "number_between": [10,20] }, 3 | "number2": [ { "number_between": [10,20] } ], 4 | "number3": [ { "number_between": [-10,10] } ], 5 | "number4": [ { "number_between": [-10,10] } ], 6 | "empty_field": { "number_between": [10,20] }, 7 | 8 | "value_is_string": { "number_between": [10,20] }, 9 | "value_is_hash": { "number_between": [10,20] }, 10 | "value_is_empty_hash": { "number_between": [10,20] }, 11 | "value_is_array": { "number_between": [10,20] }, 12 | "value_is_empty_array": { "number_between": [10,20] } 13 | } 14 | -------------------------------------------------------------------------------- /t/test_suite/negative/28-variable_object/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "-10", 3 | "product1": { 4 | "product_type": "material", 5 | "material_id": 0, 6 | "quantity": "" 7 | }, 8 | "product2": { 9 | "product_type": "material", 10 | "material_id": 100, 11 | "quantity": 10, 12 | "warehouse_id": -100 13 | }, 14 | "product3": { 15 | "product_type": "service", 16 | "name": "Very Long Name" 17 | }, 18 | "product4": { 19 | "product_type": "wrongtype", 20 | "name": "Wrongtype" 21 | }, 22 | "product5": "Some Wrong Data", 23 | "users": "not an array" 24 | } 25 | -------------------------------------------------------------------------------- /t/test_suite/negative/21-list_of_different_objects/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": ["required", "positive_integer"], 3 | "products": ["required", { "list_of_different_objects": [ 4 | "product_type", { 5 | "material": { 6 | "product_type": "required", 7 | "material_id": ["required", "positive_integer"], 8 | "quantity": ["required", {"min_number": 1} ], 9 | "warehouse_id": "positive_integer" 10 | }, 11 | "service": { 12 | "product_type": "required", 13 | "name": ["required", {"max_length": 10} ] 14 | } 15 | } 16 | ]}] 17 | } -------------------------------------------------------------------------------- /t/test_suite/negative/21-list_of_different_objects/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": "-10", 3 | "products": [ 4 | { 5 | "product_type": "material", 6 | "material_id": 0, 7 | "quantity": "" 8 | }, 9 | { 10 | "product_type": "material", 11 | "material_id": 100, 12 | "quantity": 10, 13 | "warehouse_id": -100 14 | }, 15 | { 16 | "product_type": "service", 17 | "name": "Very Long Name" 18 | }, 19 | { 20 | "product_type": "wrongtype", 21 | "name": "Wrongtype" 22 | }, 23 | "Some Wrong Data" 24 | ], 25 | "users": "not an array" 26 | } -------------------------------------------------------------------------------- /lib/rules/meta/nested_object.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | const util = require('../../util'); 3 | 4 | function nested_object(livr, ruleBuilders) { 5 | const validator = new Validator(livr).registerRules(ruleBuilders).prepare(); 6 | 7 | return (nestedObject, params, outputArr) => { 8 | if (util.isNoValue(nestedObject)) return; 9 | if (!util.isObject(nestedObject)) return 'FORMAT_ERROR'; 10 | 11 | const result = validator.validate(nestedObject); 12 | 13 | if (result) { 14 | outputArr.push(result); 15 | return; 16 | } else { 17 | return validator.getErrors(); 18 | } 19 | }; 20 | } 21 | 22 | module.exports = nested_object; -------------------------------------------------------------------------------- /lib/rules/meta/or.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'or' rule 2 | import type { RuleTypeDef, InferRuleType, LIVRRuleDefinition } from '../../../types/inference'; 3 | 4 | /** Helper type to build union from array of rule definitions */ 5 | type BuildOrUnion = 6 | Rules extends readonly [infer First extends LIVRRuleDefinition, ...infer Rest extends readonly LIVRRuleDefinition[]] 7 | ? InferRuleType | BuildOrUnion 8 | : never; 9 | 10 | declare module '../../../types/inference' { 11 | interface RuleTypeRegistry { 12 | or: RuleTypeDef< 13 | (rules: R) => BuildOrUnion, 14 | false, 15 | false 16 | >; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/rules/meta-async/nested_object.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | const util = require('../../util'); 3 | 4 | function nested_object(livr, ruleBuilders) { 5 | const validator = new AsyncValidator(livr).registerRules(ruleBuilders).prepare(); 6 | 7 | return async (nestedObject, params, outputArr) => { 8 | if (util.isNoValue(nestedObject)) return; 9 | if (!util.isObject(nestedObject)) return 'FORMAT_ERROR'; 10 | 11 | try { 12 | const result = await validator.validate(nestedObject); 13 | outputArr.push(result); 14 | return; 15 | } catch (errors) { 16 | return errors; 17 | } 18 | }; 19 | } 20 | 21 | module.exports = nested_object; -------------------------------------------------------------------------------- /lib/rules/special/email.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | const EMAIL_RE = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 4 | const UNDERSCORE_AFTER_AT = /@.*_/; 5 | const DOUBLE_AT = /@.*@/; 6 | 7 | function email() { 8 | return value => { 9 | if (util.isNoValue(value)) return; 10 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 11 | 12 | const strValue = value + ''; 13 | if (UNDERSCORE_AFTER_AT.test(strValue)) return 'WRONG_EMAIL'; 14 | if (DOUBLE_AT.test(strValue)) return 'WRONG_EMAIL'; 15 | if (!EMAIL_RE.test(strValue)) return 'WRONG_EMAIL'; 16 | 17 | return; 18 | }; 19 | } 20 | 21 | module.exports = email; -------------------------------------------------------------------------------- /lib/rules/special/url.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | // Pre-compiled regex at module load time (not per-call) 4 | const URL_RE = /^(?:(?:http|https):\/\/)(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[0-1]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))\.?|localhost)(?::\d{2,5})?(?:[/?#]\S*)?$/i; 5 | 6 | function url() { 7 | return value => { 8 | if (util.isNoValue(value)) return; 9 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 10 | 11 | if (value.length < 2083 && URL_RE.test(value)) return; 12 | return 'WRONG_URL'; 13 | }; 14 | } 15 | 16 | module.exports = url; -------------------------------------------------------------------------------- /t/test_suite/negative/29-or/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": { "or": ["email", "positive_integer" ] }, 3 | "id2-1": [{ "or": ["email", "positive_integer" ] }, "to_lc"], 4 | "id3-1": { "or": [{"min_length": 15}, ["email", "to_lc"] ] }, 5 | "id4-1": { "or": [["required", "positive_integer"], ["not_empty", "email"] ] }, 6 | 7 | 8 | "products": {"list_of": { "or": [ 9 | {"nested_object": { 10 | "product_type": ["required", {"eq": "material"}], 11 | "material_id": ["required", "positive_integer"], 12 | "quantity": ["required", {"min_number": 1} ], 13 | "warehouse_id": "positive_integer" 14 | }}, 15 | 16 | {"nested_object": { 17 | "product_type": ["required", {"eq": "service"}], 18 | "name": ["required", {"max_length": 20} ] 19 | }} 20 | ]}} 21 | } 22 | -------------------------------------------------------------------------------- /t/test_suite/aliases_negative/03-adult_age_in_user/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "adult_age", 4 | "rules": ["positive_integer", {"min_number": 18}] 5 | }, 6 | 7 | { 8 | "name": "adult_age_with_custom_error", 9 | "rules": ["positive_integer", {"min_number": 18}], 10 | "error": "WRONG_AGE" 11 | }, 12 | 13 | { 14 | "name": "user", 15 | "rules": { "nested_object":{ 16 | "name": "required", 17 | "age1": "adult_age", 18 | "age2": "adult_age_with_custom_error" 19 | }} 20 | }, 21 | 22 | { 23 | "name": "user_with_custom_error", 24 | "rules": { "nested_object":{ 25 | "name": "required", 26 | "age1": "adult_age", 27 | "age2": "adult_age_with_custom_error" 28 | }}, 29 | "error": "WRONG_USER" 30 | } 31 | ] -------------------------------------------------------------------------------- /t/test_suite/aliases_positive/03-adult_age_in_user/aliases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "adult_age", 4 | "rules": ["positive_integer", {"min_number": 18}] 5 | }, 6 | 7 | { 8 | "name": "adult_age_with_custom_error", 9 | "rules": ["positive_integer", {"min_number": 18}], 10 | "error": "WRONG_AGE" 11 | }, 12 | 13 | { 14 | "name": "user", 15 | "rules": { "nested_object":{ 16 | "name": "required", 17 | "age1": "adult_age", 18 | "age2": "adult_age_with_custom_error" 19 | }} 20 | }, 21 | 22 | { 23 | "name": "user_with_custom_error", 24 | "rules": { "nested_object":{ 25 | "name": "required", 26 | "age1": "adult_age", 27 | "age2": "adult_age_with_custom_error" 28 | }}, 29 | "error": "WRONG_USER" 30 | } 31 | ] -------------------------------------------------------------------------------- /lib/rules/string/one_of.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | function one_of(allowedValues) { 4 | if (!Array.isArray(allowedValues)) { 5 | allowedValues = Array.prototype.slice.call(arguments); 6 | allowedValues.pop(); // pop ruleBuilders 7 | } 8 | 9 | // Pre-convert all allowed values once at factory time 10 | const strAllowedValues = allowedValues.map(v => String(v)); 11 | 12 | return (value, params, outputArr) => { 13 | if (util.isNoValue(value)) return; 14 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 15 | 16 | const strValue = String(value); 17 | const index = strAllowedValues.indexOf(strValue); 18 | if (index !== -1) { 19 | outputArr.push(allowedValues[index]); 20 | return; 21 | } 22 | 23 | return 'NOT_ALLOWED_VALUE'; 24 | }; 25 | } 26 | 27 | module.exports = one_of; 28 | -------------------------------------------------------------------------------- /lib/rules/meta/variable_object.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'variable_object' rule 2 | import type { RuleTypeDef, InferFromSchema, LIVRSchema, Simplify } from '../../../types/inference'; 3 | 4 | /** Helper type to build discriminated union from schema map */ 5 | type BuildVariableObject< 6 | D extends string, 7 | Schemas extends Record 8 | > = { 9 | [K in keyof Schemas]: Simplify< 10 | InferFromSchema & { readonly [P in D]: K } 11 | >; 12 | }[keyof Schemas]; 13 | 14 | declare module '../../../types/inference' { 15 | interface RuleTypeRegistry { 16 | variable_object: RuleTypeDef< 17 | < 18 | D extends string, 19 | Schemas extends Record 20 | >(args: readonly [D, Schemas]) => BuildVariableObject, 21 | false, 22 | false 23 | >; 24 | variableObject: RuleTypeRegistry['variable_object']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /t/test_suite/negative/16-email/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "email", 3 | "email2": { "email": [] }, 4 | "email3": [ { "email": [] } ], 5 | "email4": "email", 6 | "email5": "email", 7 | "email6": "email", 8 | "email7": "email", 9 | "email8": "email", 10 | "email9": "email", 11 | "email10": "email", 12 | "email11": "email", 13 | "email12": "email", 14 | "email13": "email", 15 | "email14": "email", 16 | "email15": "email", 17 | "email16": "email", 18 | "email17": "email", 19 | "email18": "email", 20 | "email19": "email", 21 | "email20": "email", 22 | "email21": "email", 23 | "email22": "email", 24 | "email23": "email", 25 | "email24": "email", 26 | 27 | "empty_field": "email", 28 | 29 | "value_is_hash": "email", 30 | "value_is_empty_hash": "email", 31 | "value_is_array": "email", 32 | "value_is_empty_array": "email" 33 | } -------------------------------------------------------------------------------- /lib/rules/meta/or.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | 3 | function or() { 4 | const ruleSets = Array.prototype.slice.call(arguments); 5 | const ruleBuilders = ruleSets.pop(); 6 | 7 | const validators = ruleSets.map(rules => { 8 | const livr = { field: rules }; 9 | const validator = new Validator(livr).registerRules(ruleBuilders).prepare(); 10 | 11 | return validator; 12 | }); 13 | 14 | return (value, params, outputArr) => { 15 | let lastError; 16 | 17 | for (const validator of validators) { 18 | const result = validator.validate({ field: value }); 19 | 20 | if (result) { 21 | outputArr.push(result.field); 22 | return; 23 | } else { 24 | lastError = validator.getErrors().field; 25 | } 26 | } 27 | 28 | return lastError; 29 | }; 30 | } 31 | 32 | module.exports = or; -------------------------------------------------------------------------------- /t/test_suite/positive/16-email/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "email", 3 | "email2": { "email": [] }, 4 | "email3": [ { "email": [] } ], 5 | "email4": "email", 6 | "email5": "email", 7 | "email6": "email", 8 | "email7": "email", 9 | "email8": "email", 10 | "email9": "email", 11 | "email10": "email", 12 | "email11": "email", 13 | "email12": "email", 14 | "email13": "email", 15 | "email14": "email", 16 | "email15": "email", 17 | "email16": "email", 18 | "email17": "email", 19 | "email18": "email", 20 | "email19": "email", 21 | "email20": "email", 22 | "email21": "email", 23 | "email22": "email", 24 | "email23": "email", 25 | "email24": "email", 26 | "email25": "email", 27 | "email26": "email", 28 | "email27": "email", 29 | "email28": "email", 30 | "email29": "email", 31 | "email30": "email", 32 | "email31": "email", 33 | "empty_field": "email" 34 | } -------------------------------------------------------------------------------- /lib/rules/meta-async/or.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | 3 | function or() { 4 | const ruleSets = Array.prototype.slice.call(arguments); 5 | const ruleBuilders = ruleSets.pop(); 6 | 7 | const validators = ruleSets.map(rules => { 8 | const livr = { field: rules }; 9 | const validator = new AsyncValidator(livr).registerRules(ruleBuilders).prepare(); 10 | 11 | return validator; 12 | }); 13 | 14 | return async (value, params, outputArr) => { 15 | let lastError; 16 | 17 | for (const validator of validators) { 18 | try { 19 | const result = await validator.validate({ field: value }); 20 | outputArr.push(result.field); 21 | return; 22 | } catch (errors) { 23 | lastError = errors.field; 24 | } 25 | } 26 | 27 | return lastError; 28 | }; 29 | } 30 | 31 | module.exports = or; -------------------------------------------------------------------------------- /t/test_suite/positive/29-or/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1-1": { "or": ["email", "positive_integer" ] }, 3 | "id1-2": { "or": ["email", "positive_integer" ] }, 4 | 5 | "id2-1": [{ "or": ["email", "positive_integer" ] }, "to_lc"], 6 | "id2-2": [{ "or": ["email", "positive_integer" ] }, "to_lc"], 7 | 8 | "id3-1": { "or": [{"min_length": 15}, ["email", "to_lc"] ] }, 9 | "id3-2": { "or": [{"min_length": 15}, ["email", "to_lc"] ] }, 10 | 11 | "products": {"list_of": { "or": [ 12 | {"nested_object": { 13 | "product_type": ["required", {"eq": "material"}], 14 | "material_id": ["required", "positive_integer"], 15 | "quantity": ["required", {"min_number": 1} ], 16 | "warehouse_id": "positive_integer" 17 | }}, 18 | 19 | {"nested_object": { 20 | "product_type": ["required", {"eq": "service"}], 21 | "name": ["required", {"max_length": 20} ] 22 | }} 23 | ]}} 24 | } 25 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of_different_objects.d.ts: -------------------------------------------------------------------------------- 1 | // Type inference for 'list_of_different_objects' rule 2 | import type { RuleTypeDef, InferFromSchema, LIVRSchema, Simplify } from '../../../types/inference'; 3 | 4 | /** Helper type to build discriminated union from schema map */ 5 | type BuildDiscriminatedUnion< 6 | D extends string, 7 | Schemas extends Record 8 | > = { 9 | [K in keyof Schemas]: Simplify< 10 | InferFromSchema & { readonly [P in D]: K } 11 | >; 12 | }[keyof Schemas]; 13 | 14 | declare module '../../../types/inference' { 15 | interface RuleTypeRegistry { 16 | list_of_different_objects: RuleTypeDef< 17 | < 18 | D extends string, 19 | Schemas extends Record 20 | >(args: readonly [D, Schemas]) => Array>, 21 | false, 22 | false 23 | >; 24 | listOfDifferentObjects: RuleTypeRegistry['list_of_different_objects']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /t/test_suite/positive/21-list_of_different_objects/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": ["required", "positive_integer"], 3 | "products": ["required", { "list_of_different_objects": [ 4 | "product_type", { 5 | "material": { 6 | "product_type": "required", 7 | "material_id": ["required", "positive_integer"], 8 | "quantity": ["required", {"min_number": 1} ], 9 | "warehouse_id": "positive_integer" 10 | }, 11 | "service": { 12 | "product_type": "required", 13 | "name": ["required", {"max_length": 10} ] 14 | } 15 | } 16 | ]}], 17 | "empty_list": ["required", { "list_of_different_objects": [ 18 | "item_type", { 19 | "string": { 20 | "value": "required" 21 | }, 22 | "number": { 23 | "value": ["required", "integer" ] 24 | } 25 | } 26 | ]}] 27 | } -------------------------------------------------------------------------------- /lib/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 | isObject(obj) { 15 | return obj?.constructor === Object; 16 | }, 17 | 18 | isEmptyObject(obj) { 19 | for (const key in obj) { 20 | return false; 21 | } 22 | return true; 23 | }, 24 | 25 | escapeRegExp(str) { 26 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 27 | }, 28 | 29 | isNoValue(value) { 30 | return value === undefined || value === null || value === ''; 31 | }, 32 | 33 | camelize(str) { 34 | return (str + '').replace(/[_](\w|$)/g, (_, firstLetter) => firstLetter.toUpperCase()); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /t/test_suite/negative/16-email/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "WRONG_EMAIL", 3 | "email2": "WRONG_EMAIL", 4 | "email3": "WRONG_EMAIL", 5 | "email4": "WRONG_EMAIL", 6 | "email5": "WRONG_EMAIL", 7 | "email6": "WRONG_EMAIL", 8 | "email7": "WRONG_EMAIL", 9 | "email8": "WRONG_EMAIL", 10 | "email9": "WRONG_EMAIL", 11 | "email10": "WRONG_EMAIL", 12 | "email11": "WRONG_EMAIL", 13 | "email12": "WRONG_EMAIL", 14 | "email13": "WRONG_EMAIL", 15 | "email14": "WRONG_EMAIL", 16 | "email15": "WRONG_EMAIL", 17 | "email16": "WRONG_EMAIL", 18 | "email17": "WRONG_EMAIL", 19 | "email18": "WRONG_EMAIL", 20 | "email19": "WRONG_EMAIL", 21 | "email20": "WRONG_EMAIL", 22 | "email21": "WRONG_EMAIL", 23 | "email22": "WRONG_EMAIL", 24 | "email23": "WRONG_EMAIL", 25 | "email24": "WRONG_EMAIL", 26 | 27 | "value_is_hash": "FORMAT_ERROR", 28 | "value_is_empty_hash": "FORMAT_ERROR", 29 | "value_is_array": "FORMAT_ERROR", 30 | "value_is_empty_array": "FORMAT_ERROR" 31 | } -------------------------------------------------------------------------------- /t/tests-sync/04-custom_aliases.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../lib/LIVR'); 3 | 4 | LIVR.Validator.registerAliasedDefaultRule({ 5 | name: 'strong_password1', 6 | rules: { min_length: 8 }, 7 | error: 'WEAK_PASSWORD1' 8 | }); 9 | 10 | test('Validate data with registered rules', t => { 11 | const validator = new LIVR.Validator({ 12 | password1: 'strong_password1', 13 | password2: 'strong_password2' 14 | }); 15 | 16 | validator.registerAliasedRule({ 17 | name: 'strong_password2', 18 | rules: { min_length: 8 }, 19 | error: 'WEAK_PASSWORD2' 20 | }); 21 | 22 | const output = validator.validate({ 23 | password1: 'mypass', 24 | password2: 'mypass' 25 | }); 26 | 27 | t.true(!output, 'should return false due to validation errors'); 28 | 29 | t.deepEqual( 30 | validator.getErrors(), 31 | { 32 | password1: 'WEAK_PASSWORD1', 33 | password2: 'WEAK_PASSWORD2' 34 | }, 35 | 'Should contain error codes' 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /t/tests-async/04-custom_aliases.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../async'); 3 | 4 | LIVR.AsyncValidator.registerAliasedDefaultRule({ 5 | name: 'strong_password1', 6 | rules: { min_length: 8 }, 7 | error: 'WEAK_PASSWORD1' 8 | }); 9 | 10 | test('Validate data with registered rules', async (t) => { 11 | const validator = new LIVR.AsyncValidator({ 12 | password1: 'strong_password1', 13 | password2: 'strong_password2' 14 | }); 15 | 16 | validator.registerAliasedRule({ 17 | name: 'strong_password2', 18 | rules: { min_length: 8 }, 19 | error: 'WEAK_PASSWORD2' 20 | }); 21 | 22 | try { 23 | const output = await validator.validate({ 24 | password1: 'mypass', 25 | password2: 'mypass' 26 | }); 27 | } catch (errors) { 28 | t.deepEqual( 29 | errors, 30 | { 31 | password1: 'WEAK_PASSWORD1', 32 | password2: 'WEAK_PASSWORD2' 33 | }, 34 | 'Should contain error codes' 35 | ); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/rules/meta/variable_object.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | const util = require('../../util'); 3 | 4 | function variable_object(selectorField, livrs, ruleBuilders) { 5 | const validators = {}; 6 | 7 | for (const selectorValue in livrs) { 8 | const validator = new Validator(livrs[selectorValue]) 9 | .registerRules(ruleBuilders) 10 | .prepare(); 11 | validators[selectorValue] = validator; 12 | } 13 | 14 | return (object, params, outputArr) => { 15 | if (util.isNoValue(object)) return; 16 | 17 | if ( 18 | !util.isObject(object) || 19 | !object[selectorField] || 20 | !validators[object[selectorField]] 21 | ) { 22 | return 'FORMAT_ERROR'; 23 | } 24 | 25 | const validator = validators[object[selectorField]]; 26 | const result = validator.validate(object); 27 | 28 | if (result) { 29 | outputArr.push(result); 30 | return; 31 | } else { 32 | return validator.getErrors(); 33 | } 34 | }; 35 | } 36 | 37 | module.exports = variable_object; 38 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of_objects.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | const util = require('../../util'); 3 | 4 | function list_of_objects(livr, ruleBuilders) { 5 | const validator = new Validator(livr).registerRules(ruleBuilders).prepare(); 6 | 7 | return (objects, params, outputArr) => { 8 | if (util.isNoValue(objects)) return; 9 | if (!Array.isArray(objects)) return 'FORMAT_ERROR'; 10 | 11 | const results = []; 12 | const errors = []; 13 | let hasErrors = false; 14 | 15 | for (const object of objects) { 16 | const result = validator.validate(object); 17 | 18 | if (result) { 19 | results.push(result); 20 | errors.push(null); 21 | } else { 22 | hasErrors = true; 23 | errors.push(validator.getErrors()); 24 | results.push(null); 25 | } 26 | } 27 | 28 | if (hasErrors) { 29 | return errors; 30 | } else { 31 | outputArr.push(results); 32 | return; 33 | } 34 | }; 35 | } 36 | 37 | module.exports = list_of_objects; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | v2.9.4 4 | - Type inference: `default` rule now widens literal types to primitives (e.g., `{default: 10}` infers as `number` instead of `10`) 5 | - Use type assertions with unions to preserve specific types (e.g., `{default: 'ACTIVE' as 'ACTIVE' | 'PENDING'}`) 6 | - Add TypeScript type inference tests (`t/types-test.ts`) 7 | - Add `tsconfig.json` for type checking 8 | - `npm test` now includes TypeScript type checks 9 | 10 | v2.9.3 11 | - Add support for custom templates for complex cases of type inference 12 | 13 | v2.9 14 | 15 | - Add types inference engine 16 | 17 | v2.8.1 18 | 19 | - Add basic types for typescript 20 | 21 | v2.7.1 22 | 23 | - 300-500% faster validator construction. Useful when you construct new Validator for each validation. 24 | - Remove "camelizeRules" option. Now it is always enabled but does not overried existing rules if there is a conflict. 25 | 26 | v2.7 27 | 28 | - 25% faster validation comparing to v2.5 29 | - Automatically camelize default rules (both names can be used in schemas) 30 | - Validator constructor now accepts "options" object. Required for future options extensability 31 | -------------------------------------------------------------------------------- /lib/rules/meta-async/variable_object.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | const util = require('../../util'); 3 | 4 | function variable_object(selectorField, livrs, ruleBuilders) { 5 | const validators = {}; 6 | 7 | for (const selectorValue in livrs) { 8 | const validator = new AsyncValidator(livrs[selectorValue]) 9 | .registerRules(ruleBuilders) 10 | .prepare(); 11 | validators[selectorValue] = validator; 12 | } 13 | 14 | return async (object, params, outputArr) => { 15 | if (util.isNoValue(object)) return; 16 | 17 | if ( 18 | !util.isObject(object) || 19 | !object[selectorField] || 20 | !validators[object[selectorField]] 21 | ) { 22 | return 'FORMAT_ERROR'; 23 | } 24 | 25 | const validator = validators[object[selectorField]]; 26 | 27 | try { 28 | const result = await validator.validate(object); 29 | outputArr.push(result); 30 | return; 31 | } catch (errors) { 32 | return errors; 33 | } 34 | }; 35 | } 36 | 37 | module.exports = variable_object; 38 | -------------------------------------------------------------------------------- /lib/rules/meta-async/list_of_objects.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | const util = require('../../util'); 3 | 4 | function list_of_objects(livr, ruleBuilders) { 5 | const validator = new AsyncValidator(livr).registerRules(ruleBuilders).prepare(); 6 | 7 | return async (objects, params, outputArr) => { 8 | if (util.isNoValue(objects)) return; 9 | if (!Array.isArray(objects)) return 'FORMAT_ERROR'; 10 | 11 | const results = []; 12 | const errors = []; 13 | let hasErrors = false; 14 | 15 | for (const object of objects) { 16 | try { 17 | const result = await validator.validate(object); 18 | results.push(result); 19 | errors.push(null); 20 | } catch (caughtErrors) { 21 | hasErrors = true; 22 | errors.push(caughtErrors); 23 | results.push(null); 24 | } 25 | } 26 | 27 | if (hasErrors) { 28 | return errors; 29 | } else { 30 | outputArr.push(results); 31 | return; 32 | } 33 | }; 34 | } 35 | 36 | module.exports = list_of_objects; -------------------------------------------------------------------------------- /t/test_suite/negative/16-email/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "testmail.com", 3 | "email2": "test@test@mail.com.ua", 4 | "email3": "test+test.t_e_s_t@ma_il.in.ua", 5 | "email4": "user name@gmail.com", 6 | "email5": "username@mail.c om", 7 | "email6": "en ny@enny.devoffice.com", 8 | "email7": "enny..tester@devoffice.com", 9 | "email8": ".enny@enny.devoffice.com", 10 | "email9": "enny.devoffice.com", 11 | "email10": "test@test", 12 | "email11": "tester@enny..devoffice.com", 13 | "email12": "en(ny@enny.devoffice.com", 14 | "email13": "en)ny@enny.devoffice.com", 15 | "email14": "en[enny@devoffice.com", 16 | "email15": "en]ny@devoffice.com", 17 | "email16": "ho:rhe@tester.devoffice.com", 18 | "email17": "enny;tester@devoffice.com", 19 | "email18": "te,st@devoffice.com", 20 | "email19": "testter@devoffice.com", 22 | "email21": "tester@@devoffice.com", 23 | "email22": "tester@tes ter.devoffice.com", 24 | "email23": "@enny tm.devoffice.com", 25 | "email24": "онстерович@письмо.рф ", 26 | 27 | "empty_field": "", 28 | "extra_field": "aaaa", 29 | 30 | "value_is_hash": {"test": 1}, 31 | "value_is_empty_hash": {}, 32 | "value_is_array": ["test", 1], 33 | "value_is_empty_array": [] 34 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | "postCreateCommand": "npm install -g @anthropic-ai/claude-code", 16 | 17 | // Configure tool-specific properties. 18 | "customizations": { 19 | "vscode": { 20 | "extensions": [ 21 | "esbenp.prettier-vscode", 22 | "mechatroner.rainbow-csv", 23 | "Anthropic.claude-code" 24 | ] 25 | } 26 | } 27 | 28 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 29 | // "remoteUser": "root" 30 | } 31 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | const util = require('../../util'); 3 | 4 | function list_of(rules, ruleBuilders) { 5 | if (!Array.isArray(rules)) { 6 | rules = Array.prototype.slice.call(arguments); 7 | ruleBuilders = rules.pop(); 8 | } 9 | 10 | const livr = { field: rules }; 11 | const validator = new Validator(livr).registerRules(ruleBuilders).prepare(); 12 | 13 | return (values, params, outputArr) => { 14 | if (util.isNoValue(values)) return; 15 | 16 | if (!Array.isArray(values)) return 'FORMAT_ERROR'; 17 | 18 | const results = []; 19 | const errors = []; 20 | let hasErrors = false; 21 | 22 | for (const value of values) { 23 | const result = validator.validate({ field: value }); 24 | 25 | if (result) { 26 | results.push(result.field); 27 | errors.push(null); 28 | } else { 29 | hasErrors = true; 30 | errors.push(validator.getErrors().field); 31 | results.push(null); 32 | } 33 | } 34 | 35 | if (hasErrors) { 36 | return errors; 37 | } else { 38 | outputArr.push(results); 39 | return; 40 | } 41 | }; 42 | } 43 | 44 | module.exports = list_of; -------------------------------------------------------------------------------- /lib/rules/special/iso_date.js: -------------------------------------------------------------------------------- 1 | const util = require('../../util'); 2 | 3 | const ISO_DATE_RE = /^(\d{4})-([0-1][0-9])-([0-3][0-9])$/; 4 | 5 | // Days in each month (index 0 = January) 6 | const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 7 | 8 | function isLeapYear(year) { 9 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; 10 | } 11 | 12 | function iso_date() { 13 | return value => { 14 | if (util.isNoValue(value)) return; 15 | if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; 16 | 17 | const matched = (value + '').match(ISO_DATE_RE); 18 | 19 | if (matched) { 20 | const year = +matched[1]; 21 | const month = +matched[2]; 22 | const day = +matched[3]; 23 | 24 | // Validate month (1-12) 25 | if (month < 1 || month > 12) return 'WRONG_DATE'; 26 | 27 | // Get max days for this month 28 | let maxDays = DAYS_IN_MONTH[month - 1]; 29 | 30 | // February leap year check 31 | if (month === 2 && isLeapYear(year)) { 32 | maxDays = 29; 33 | } 34 | 35 | // Validate day 36 | if (day >= 1 && day <= maxDays) { 37 | return; 38 | } 39 | } 40 | 41 | return 'WRONG_DATE'; 42 | }; 43 | } 44 | 45 | module.exports = iso_date; -------------------------------------------------------------------------------- /lib/rules/meta-async/list_of.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | const util = require('../../util'); 3 | 4 | function list_of(rules, ruleBuilders) { 5 | if (!Array.isArray(rules)) { 6 | rules = Array.prototype.slice.call(arguments); 7 | ruleBuilders = rules.pop(); 8 | } 9 | 10 | const livr = { field: rules }; 11 | const validator = new AsyncValidator(livr).registerRules(ruleBuilders).prepare(); 12 | 13 | return async (values, params, outputArr) => { 14 | if (util.isNoValue(values)) return; 15 | 16 | if (!Array.isArray(values)) return 'FORMAT_ERROR'; 17 | 18 | const results = []; 19 | const errors = []; 20 | let hasErrors = false; 21 | 22 | for (const value of values) { 23 | try { 24 | const result = await validator.validate({ field: value }); 25 | results.push(result.field); 26 | errors.push(null); 27 | } catch (caughtErrors) { 28 | hasErrors = true; 29 | errors.push(caughtErrors.field); 30 | results.push(null); 31 | } 32 | } 33 | 34 | if (hasErrors) { 35 | return errors; 36 | } else { 37 | outputArr.push(results); 38 | return; 39 | } 40 | }; 41 | } 42 | 43 | module.exports = list_of; -------------------------------------------------------------------------------- /t/test_suite/positive/16-email/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "test@mail.com", 3 | "email2": "test.test@mail.com.ua", 4 | "email3": "test+test.t_e_s_t@mail.in.ua", 5 | "email4": "user@hotline.finance", 6 | "email5": "enny@enny.devoffice.com", 7 | "email6": "enny.tester@gmail.com", 8 | "email7": "enny#tester@mail.ru", 9 | "email8": "enny!enny@templatemonster.me", 10 | "email9": "enny@enny.devoffice.com", 11 | "email10": "enny$@enny.devoffice.com", 12 | "email11": "enny%tester@enny.devoffice.com", 13 | "email12": "enny&te@enny.devoffice.com", 14 | "email13": "enn*y@mail.ru", 15 | "email14": "en+ny@yahoo.com", 16 | "email15": "ennyqwerty@gmail.com", 17 | "email16": "ennyqwerty@yandex.com", 18 | "email17": "ennyqwerty@hotmail.com", 19 | "email18": "enny@templatemonster.me", 20 | "email19": "enny-newcheckout@templatemonster.me", 21 | "email20": "enny/tester@enny.devoffice.com", 22 | "email21": "en=ny@enny.devoffice.com", 23 | "email22": "tes?ter@enny.devoffice.com", 24 | "email23": "enny^enny@enny.devoffice.com", 25 | "email24": "enny_sk@enny.devoffice.com", 26 | "email25": "enn`y@enny.devoffice.com", 27 | "email26": "enny|sk@enny.devoffice.com", 28 | "email27": "er@enny.devoffice.com", 29 | "email28": "dd@hj.ff", 30 | "email29": "t@devoffice.com", 31 | "email30": "ennytesttesttesttesttesttesttesttesttesttesttesttesttesttesttest@te1.com", 32 | "email31": "er@enny.devoffice.com", 33 | "empty_field": "" 34 | } -------------------------------------------------------------------------------- /t/test_suite/positive/16-email/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "email1": "test@mail.com", 3 | "email2": "test.test@mail.com.ua", 4 | "email3": "test+test.t_e_s_t@mail.in.ua", 5 | "email4": "user@hotline.finance", 6 | "email5": "enny@enny.devoffice.com", 7 | "email6": "enny.tester@gmail.com", 8 | "email7": "enny#tester@mail.ru", 9 | "email8": "enny!enny@templatemonster.me", 10 | "email9": "enny@enny.devoffice.com", 11 | "email10": "enny$@enny.devoffice.com", 12 | "email11": "enny%tester@enny.devoffice.com", 13 | "email12": "enny&te@enny.devoffice.com", 14 | "email13": "enn*y@mail.ru", 15 | "email14": "en+ny@yahoo.com", 16 | "email15": "ennyqwerty@gmail.com", 17 | "email16": "ennyqwerty@yandex.com", 18 | "email17": "ennyqwerty@hotmail.com", 19 | "email18": "enny@templatemonster.me", 20 | "email19": "enny-newcheckout@templatemonster.me", 21 | "email20": "enny/tester@enny.devoffice.com", 22 | "email21": "en=ny@enny.devoffice.com", 23 | "email22": "tes?ter@enny.devoffice.com", 24 | "email23": "enny^enny@enny.devoffice.com", 25 | "email24": "enny_sk@enny.devoffice.com", 26 | "email25": "enn`y@enny.devoffice.com", 27 | "email26": "enny|sk@enny.devoffice.com", 28 | "email27": "er@enny.devoffice.com", 29 | "email28": "dd@hj.ff", 30 | "email29": "t@devoffice.com", 31 | "email30": "ennytesttesttesttesttesttesttesttesttesttesttesttesttesttesttest@te1.com", 32 | "email31": "er@enny.devoffice.com", 33 | 34 | "empty_field": "", 35 | "extra_field": "aaaa" 36 | } -------------------------------------------------------------------------------- /t/tests-sync/06-camelize_rule_names.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../lib/LIVR'); 3 | 4 | test('Should support camelized and underscore rules', (t) => { 5 | const validator = new LIVR.Validator({ 6 | name1: { maxLength: 5 }, 7 | name2: { max_length: 5 }, 8 | }); 9 | 10 | const output = validator.validate({ 11 | name1: 'myname1', 12 | name2: 'myname2', 13 | }); 14 | 15 | t.true(!output, 'should return false due to validation errors'); 16 | 17 | t.deepEqual( 18 | validator.getErrors(), 19 | { 20 | name1: 'TOO_LONG', 21 | name2: 'TOO_LONG', 22 | }, 23 | 'Should contain error codes' 24 | ); 25 | }); 26 | 27 | test('Camelization should not overide custom rules', (t) => { 28 | LIVR.Validator.registerDefaultRules({ 29 | minLength() { 30 | return () => 'MY_MIN_RULE'; 31 | }, 32 | }); 33 | 34 | const validator = new LIVR.Validator({ 35 | password1: { maxLength: 5 }, 36 | password2: { minLength: 5 }, 37 | }); 38 | 39 | validator.registerRules({ 40 | maxLength() { 41 | return () => 'MY_MAX_RULE'; 42 | }, 43 | }); 44 | 45 | const output = validator.validate({ 46 | password1: '123', 47 | password2: '123', 48 | }); 49 | 50 | t.true(!output, 'should return false due to validation errors'); 51 | 52 | t.deepEqual( 53 | validator.getErrors(), 54 | { 55 | password1: 'MY_MAX_RULE', 56 | password2: 'MY_MIN_RULE', 57 | }, 58 | 'Should contain error codes' 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /t/tests-async/06-camelize_rule_names.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../async'); 3 | 4 | test('Should support camelized and underscore rules by default', async (t) => { 5 | const validator = new LIVR.AsyncValidator({ 6 | name1: { maxLength: 5 }, 7 | name2: { max_length: 5 }, 8 | }); 9 | 10 | try { 11 | await validator.validate({ 12 | name1: 'myname1', 13 | name2: 'myname2', 14 | }); 15 | } catch (errors) { 16 | t.deepEqual( 17 | errors, 18 | { 19 | name1: 'TOO_LONG', 20 | name2: 'TOO_LONG', 21 | }, 22 | 'Should contain error codes' 23 | ); 24 | } 25 | }); 26 | 27 | test('Camelization should not overide custom rules', async (t) => { 28 | LIVR.AsyncValidator.registerDefaultRules({ 29 | minLength() { 30 | return () => 'MY_MIN_RULE'; 31 | }, 32 | }); 33 | 34 | const validator = new LIVR.AsyncValidator({ 35 | password1: { maxLength: 5 }, 36 | password2: { minLength: 5 }, 37 | }); 38 | 39 | validator.registerRules({ 40 | maxLength() { 41 | return () => 'MY_MAX_RULE'; 42 | }, 43 | }); 44 | 45 | try { 46 | await validator.validate({ 47 | password1: '123', 48 | password2: '123', 49 | }); 50 | } catch (errors) { 51 | t.deepEqual( 52 | errors, 53 | { 54 | password1: 'MY_MAX_RULE', 55 | password2: 'MY_MIN_RULE', 56 | }, 57 | 'Should contain error codes' 58 | ); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /lib/rules/meta/list_of_different_objects.js: -------------------------------------------------------------------------------- 1 | const Validator = require('../../Validator'); 2 | const util = require('../../util'); 3 | 4 | function list_of_different_objects(selectorField, livrs, ruleBuilders) { 5 | const validators = {}; 6 | 7 | for (const selectorValue in livrs) { 8 | const validator = new Validator(livrs[selectorValue]) 9 | .registerRules(ruleBuilders) 10 | .prepare(); 11 | validators[selectorValue] = validator; 12 | } 13 | 14 | return (objects, params, outputArr) => { 15 | if (util.isNoValue(objects)) return; 16 | if (!Array.isArray(objects)) return 'FORMAT_ERROR'; 17 | 18 | const results = []; 19 | const errors = []; 20 | let hasErrors = false; 21 | 22 | for (const object of objects) { 23 | if ( 24 | typeof object != 'object' || 25 | !object[selectorField] || 26 | !validators[object[selectorField]] 27 | ) { 28 | errors.push('FORMAT_ERROR'); 29 | continue; 30 | } 31 | 32 | const validator = validators[object[selectorField]]; 33 | const result = validator.validate(object); 34 | 35 | if (result) { 36 | results.push(result); 37 | errors.push(null); 38 | } else { 39 | hasErrors = true; 40 | errors.push(validator.getErrors()); 41 | results.push(null); 42 | } 43 | } 44 | 45 | if (hasErrors) { 46 | return errors; 47 | } else { 48 | outputArr.push(results); 49 | return; 50 | } 51 | }; 52 | } 53 | 54 | module.exports = list_of_different_objects; -------------------------------------------------------------------------------- /t/test_suite/positive/28-variable_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": ["required", "positive_integer"], 3 | "product1": ["required", { "variable_object": [ 4 | "product_type", { 5 | "material": { 6 | "product_type": "required", 7 | "material_id": ["required", "positive_integer"], 8 | "quantity": ["required", {"min_number": 1} ], 9 | "warehouse_id": "positive_integer" 10 | }, 11 | "service": { 12 | "product_type": "required", 13 | "name": ["required", {"max_length": 10} ] 14 | } 15 | } 16 | ]}], 17 | 18 | "product2": ["required", { "variable_object": [ 19 | "product_type", { 20 | "material": { 21 | "product_type": "required", 22 | "material_id": ["required", "positive_integer"], 23 | "quantity": ["required", {"min_number": 1} ], 24 | "warehouse_id": "positive_integer" 25 | }, 26 | "service": { 27 | "product_type": "required", 28 | "name": ["required", {"max_length": 10} ] 29 | } 30 | } 31 | ]}], 32 | 33 | "product3": ["required", { "variable_object": [ 34 | "product_type", { 35 | "material": { 36 | "product_type": "required", 37 | "material_id": ["required", "positive_integer"], 38 | "quantity": ["required", {"min_number": 1} ], 39 | "warehouse_id": "positive_integer" 40 | }, 41 | "service": { 42 | "product_type": "required", 43 | "name": ["required", {"max_length": 10} ] 44 | } 45 | } 46 | ]}] 47 | } 48 | -------------------------------------------------------------------------------- /t/tests-sync/02-auto_trim.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../lib/LIVR'); 3 | 4 | const validator = new LIVR.Validator( 5 | { 6 | code: 'required', 7 | password: ['required', { min_length: 3 }], 8 | list: { list_of: ['string'] }, 9 | address: { 10 | nested_object: { 11 | street: { min_length: 5 } 12 | } 13 | } 14 | }, 15 | true 16 | ); 17 | 18 | test('NEGATIVE: Validate data with automatic trim', t => { 19 | const output = validator.validate({ 20 | code: ' ', 21 | password: ' 12 ', 22 | address: { 23 | street: ' hell ' 24 | } 25 | }); 26 | 27 | t.true(!output, 'should return false due to validation errors for trimmed values'); 28 | 29 | t.deepEqual( 30 | validator.getErrors(), 31 | { 32 | code: 'REQUIRED', 33 | password: 'TOO_SHORT', 34 | address: { 35 | street: 'TOO_SHORT' 36 | } 37 | }, 38 | 'Should contain error codes' 39 | ); 40 | }); 41 | 42 | test('POSITIVE: Validate data with automatic trim', t => { 43 | const cleanData = validator.validate({ 44 | code: ' A ', 45 | password: ' 123 ', 46 | list: [' aaa ', ' bbb '], 47 | address: { 48 | street: ' hello ' 49 | } 50 | }); 51 | 52 | t.truthy(cleanData, 'should return clean data'); 53 | 54 | t.deepEqual( 55 | cleanData, 56 | { 57 | code: 'A', 58 | password: '123', 59 | list: ['aaa', 'bbb'], 60 | address: { 61 | street: 'hello' 62 | } 63 | }, 64 | 'Should contain error codes' 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/rules/meta-async/list_of_different_objects.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('../../AsyncValidator'); 2 | const util = require('../../util'); 3 | 4 | function list_of_different_objects(selectorField, livrs, ruleBuilders) { 5 | const validators = {}; 6 | 7 | for (const selectorValue in livrs) { 8 | const validator = new AsyncValidator(livrs[selectorValue]) 9 | .registerRules(ruleBuilders) 10 | .prepare(); 11 | validators[selectorValue] = validator; 12 | } 13 | 14 | return async (objects, params, outputArr) => { 15 | if (util.isNoValue(objects)) return; 16 | if (!Array.isArray(objects)) return 'FORMAT_ERROR'; 17 | 18 | const results = []; 19 | const errors = []; 20 | let hasErrors = false; 21 | 22 | for (const object of objects) { 23 | if ( 24 | typeof object != 'object' || 25 | !object[selectorField] || 26 | !validators[object[selectorField]] 27 | ) { 28 | errors.push('FORMAT_ERROR'); 29 | continue; 30 | } 31 | 32 | const validator = validators[object[selectorField]]; 33 | 34 | try { 35 | const result = await validator.validate(object); 36 | results.push(result); 37 | errors.push(null); 38 | } catch (caughtErrors) { 39 | hasErrors = true; 40 | errors.push(caughtErrors); 41 | results.push(null); 42 | } 43 | } 44 | 45 | if (hasErrors) { 46 | return errors; 47 | } else { 48 | outputArr.push(results); 49 | return; 50 | } 51 | }; 52 | } 53 | 54 | module.exports = list_of_different_objects; -------------------------------------------------------------------------------- /t/tests-sync/03-custom_filters.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../lib/LIVR'); 3 | 4 | LIVR.Validator.registerDefaultRules({ 5 | my_trim() { 6 | return (value, undefined, outputArr) => { 7 | if (value === undefined || value === null || typeof value === 'object' || value === '') 8 | return; 9 | 10 | value += ''; 11 | outputArr.push(value.replace(/^\s*/, '').replace(/\s*$/, '')); 12 | }; 13 | }, 14 | 15 | my_lc() { 16 | return (value, undefined, outputArr) => { 17 | if (value === undefined || value === null || typeof value === 'object' || value === '') 18 | return; 19 | 20 | value += ''; 21 | outputArr.push(value.toLowerCase()); 22 | }; 23 | }, 24 | 25 | my_ucfirst() { 26 | return (value, undefined, outputArr) => { 27 | if (value === undefined || value === null || typeof value === 'object' || value === '') 28 | return; 29 | 30 | value += ''; 31 | outputArr.push(value.charAt(0).toUpperCase() + value.slice(1)); 32 | }; 33 | } 34 | }); 35 | 36 | test('Validate data with registered rules', t => { 37 | const validator = new LIVR.Validator({ 38 | word1: ['my_trim', 'my_lc', 'my_ucfirst'], 39 | word2: ['my_trim', 'my_lc'], 40 | word3: ['my_ucfirst'] 41 | }); 42 | 43 | const output = validator.validate({ 44 | word1: ' wordOne ', 45 | word2: ' wordTwo ', 46 | word3: 'wordThree ' 47 | }); 48 | 49 | t.deepEqual( 50 | output, 51 | { 52 | word1: 'Wordone', 53 | word2: 'wordtwo', 54 | word3: 'WordThree ' 55 | }, 56 | 'Should apply changes to values' 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /t/tests-async/02-auto_trim.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../async'); 3 | 4 | const validator = new LIVR.AsyncValidator( 5 | { 6 | code: 'required', 7 | password: ['required', { min_length: 3 }], 8 | list: { list_of: ['string'] }, 9 | address: { 10 | nested_object: { 11 | street: { min_length: 5 } 12 | } 13 | } 14 | }, 15 | true 16 | ); 17 | 18 | 19 | test('NEGATIVE: Validate data with automatic trim', async (t) => { 20 | try { 21 | const output = await validator.validate({ 22 | code: ' ', 23 | password: ' 12 ', 24 | address: { 25 | street: ' hell ' 26 | } 27 | }); 28 | } catch (errors) { 29 | t.deepEqual( 30 | errors, 31 | { 32 | code: 'REQUIRED', 33 | password: 'TOO_SHORT', 34 | address: { 35 | street: 'TOO_SHORT' 36 | } 37 | }, 38 | 'Should contain error codes' 39 | ); 40 | } 41 | }); 42 | 43 | test('POSITIVE: Validate data with automatic trim', async (t) => { 44 | const cleanData = await validator.validate({ 45 | code: ' A ', 46 | password: ' 123 ', 47 | list: [' aaa ', ' bbb '], 48 | address: { 49 | street: ' hello ' 50 | } 51 | }); 52 | 53 | t.truthy(cleanData, 'should return clean data'); 54 | 55 | t.deepEqual( 56 | cleanData, 57 | { 58 | code: 'A', 59 | password: '123', 60 | list: ['aaa', 'bbb'], 61 | address: { 62 | street: 'hello' 63 | } 64 | }, 65 | 'Should contain error codes' 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /t/tests-async/03-custom_filters.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../async'); 3 | 4 | LIVR.AsyncValidator.registerDefaultRules({ 5 | my_trim() { 6 | return (value, undefined, outputArr) => { 7 | if (value === undefined || value === null || typeof value === 'object' || value === '') 8 | return; 9 | 10 | value += ''; 11 | outputArr.push(value.replace(/^\s*/, '').replace(/\s*$/, '')); 12 | }; 13 | }, 14 | 15 | my_lc() { 16 | return (value, undefined, outputArr) => { 17 | if (value === undefined || value === null || typeof value === 'object' || value === '') 18 | return; 19 | 20 | value += ''; 21 | outputArr.push(value.toLowerCase()); 22 | }; 23 | }, 24 | 25 | my_ucfirst() { 26 | return (value, undefined, outputArr) => { 27 | if (value === undefined || value === null || typeof value === 'object' || value === '') 28 | return; 29 | 30 | value += ''; 31 | outputArr.push(value.charAt(0).toUpperCase() + value.slice(1)); 32 | }; 33 | } 34 | }); 35 | 36 | test('Validate data with registered rules', async (t) => { 37 | const validator = new LIVR.AsyncValidator({ 38 | word1: ['my_trim', 'my_lc', 'my_ucfirst'], 39 | word2: ['my_trim', 'my_lc'], 40 | word3: ['my_ucfirst'] 41 | }); 42 | 43 | const output = await validator.validate({ 44 | word1: ' wordOne ', 45 | word2: ' wordTwo ', 46 | word3: 'wordThree ' 47 | }); 48 | 49 | t.deepEqual( 50 | output, 51 | { 52 | word1: 'Wordone', 53 | word2: 'wordtwo', 54 | word3: 'WordThree ' 55 | }, 56 | 'Should apply changes to values' 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /t/tests-sync/05-rules_replacement.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../lib/LIVR'); 3 | 4 | function patchRule(ruleName, ruleBuilder) { 5 | return function(...params) { 6 | const ruleValidator = ruleBuilder(...params); 7 | const ruleArgs = params.splice(0, params.length - 1); 8 | 9 | return (...params) => { 10 | const errorCode = ruleValidator(...params); 11 | 12 | if (errorCode) { 13 | const rule = { 14 | [ruleName]: ruleArgs 15 | }; 16 | 17 | return { 18 | code: errorCode, 19 | rule 20 | }; 21 | } 22 | }; 23 | }; 24 | } 25 | 26 | test('Rules replacement: Validate data with registered rules', t => { 27 | // Patch rules 28 | const defaultRules = LIVR.Validator.getDefaultRules(); 29 | 30 | const originalRules = {}; 31 | const newRules = {}; 32 | 33 | for (const ruleName in defaultRules) { 34 | const ruleBuilder = defaultRules[ruleName]; 35 | originalRules[ruleName] = ruleBuilder; 36 | newRules[ruleName] = patchRule(ruleName, ruleBuilder); 37 | } 38 | 39 | LIVR.Validator.registerDefaultRules(newRules); 40 | 41 | // Test 42 | const validator = new LIVR.Validator({ 43 | name: ['required'], 44 | phone: { max_length: 10 } 45 | }); 46 | 47 | const output = validator.validate({ 48 | phone: '123456789123456' 49 | }); 50 | 51 | t.true(!output, 'Validation should fail'); 52 | 53 | t.deepEqual( 54 | validator.getErrors(), 55 | { 56 | name: { 57 | code: 'REQUIRED', 58 | rule: { required: [] } 59 | }, 60 | 61 | phone: { 62 | code: 'TOO_LONG', 63 | rule: { max_length: [10] } 64 | } 65 | }, 66 | 'Should return detailed errors' 67 | ); 68 | 69 | // Restore 70 | LIVR.Validator.registerDefaultRules(originalRules); 71 | }); 72 | -------------------------------------------------------------------------------- /t/tests-async/05-rules_replacement.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const LIVR = require('../../async'); 3 | 4 | function patchRule(ruleName, ruleBuilder) { 5 | return function(...params) { 6 | const ruleValidator = ruleBuilder(...params); 7 | const ruleArgs = params.splice(0, params.length - 1); 8 | 9 | return (...params) => { 10 | const errorCode = ruleValidator(...params); 11 | 12 | if (errorCode) { 13 | const rule = { 14 | [ruleName]: ruleArgs 15 | }; 16 | 17 | return { 18 | code: errorCode, 19 | rule 20 | }; 21 | } 22 | }; 23 | }; 24 | } 25 | 26 | test('Rules replacement: Validate data with registered rules', async (t) => { 27 | // Patch rules 28 | const defaultRules = LIVR.AsyncValidator.getDefaultRules(); 29 | 30 | const originalRules = {}; 31 | const newRules = {}; 32 | 33 | for (const ruleName in defaultRules) { 34 | const ruleBuilder = defaultRules[ruleName]; 35 | originalRules[ruleName] = ruleBuilder; 36 | newRules[ruleName] = patchRule(ruleName, ruleBuilder); 37 | } 38 | 39 | LIVR.AsyncValidator.registerDefaultRules(newRules); 40 | 41 | // Test 42 | const validator = new LIVR.AsyncValidator({ 43 | name: ['required'], 44 | phone: { max_length: 10 } 45 | }); 46 | 47 | try { 48 | const output = await validator.validate({ 49 | phone: '123456789123456' 50 | }); 51 | } catch (errors) { 52 | t.deepEqual( 53 | errors, 54 | { 55 | name: { 56 | code: 'REQUIRED', 57 | rule: { required: [] } 58 | }, 59 | 60 | phone: { 61 | code: 'TOO_LONG', 62 | rule: { max_length: [10] } 63 | } 64 | }, 65 | 'Should return detailed errors' 66 | ); 67 | } 68 | 69 | // Restore 70 | LIVR.AsyncValidator.registerDefaultRules(originalRules); 71 | }); 72 | -------------------------------------------------------------------------------- /lib/LIVR.js: -------------------------------------------------------------------------------- 1 | const Validator = require('./Validator'); 2 | const util = require('./util'); 3 | 4 | const rules = { 5 | required: require('./rules/common/required'), 6 | not_empty: require('./rules/common/not_empty'), 7 | not_empty_list: require('./rules/common/not_empty_list'), 8 | any_object: require('./rules/common/any_object'), 9 | 10 | string: require('./rules/string/string'), 11 | eq: require('./rules/string/eq'), 12 | one_of: require('./rules/string/one_of'), 13 | max_length: require('./rules/string/max_length'), 14 | min_length: require('./rules/string/min_length'), 15 | length_equal: require('./rules/string/length_equal'), 16 | length_between: require('./rules/string/length_between'), 17 | like: require('./rules/string/like'), 18 | 19 | integer: require('./rules/numeric/integer'), 20 | positive_integer: require('./rules/numeric/positive_integer'), 21 | decimal: require('./rules/numeric/decimal'), 22 | positive_decimal: require('./rules/numeric/positive_decimal'), 23 | max_number: require('./rules/numeric/max_number'), 24 | min_number: require('./rules/numeric/min_number'), 25 | number_between: require('./rules/numeric/number_between'), 26 | 27 | email: require('./rules/special/email'), 28 | equal_to_field: require('./rules/special/equal_to_field'), 29 | url: require('./rules/special/url'), 30 | iso_date: require('./rules/special/iso_date'), 31 | 32 | default: require('./rules/modifiers/default'), 33 | trim: require('./rules/modifiers/trim'), 34 | to_lc: require('./rules/modifiers/to_lc'), 35 | to_uc: require('./rules/modifiers/to_uc'), 36 | remove: require('./rules/modifiers/remove'), 37 | leave_only: require('./rules/modifiers/leave_only'), 38 | 39 | nested_object: require('./rules/meta/nested_object'), 40 | variable_object: require('./rules/meta/variable_object'), 41 | list_of: require('./rules/meta/list_of'), 42 | list_of_objects: require('./rules/meta/list_of_objects'), 43 | or: require('./rules/meta/or'), 44 | list_of_different_objects: require('./rules/meta/list_of_different_objects'), 45 | }; 46 | 47 | Validator.registerDefaultRules(rules); 48 | 49 | module.exports = { Validator, rules, util }; 50 | -------------------------------------------------------------------------------- /examples/simple.ts: -------------------------------------------------------------------------------- 1 | // examples/simple.ts 2 | // Simple TypeScript Type Inference Example 3 | // 4 | // This example demonstrates basic LIVR type inference features: 5 | // - Required and optional fields 6 | // - Primitive types (string, number) 7 | // - Literal types with `one_of` and `eq` 8 | // - Using `as const` for proper inference 9 | 10 | // When using as an npm package: 11 | // import LIVR from 'livr'; 12 | // import type { InferFromSchema } from 'livr/types'; 13 | 14 | // Within the repo, use relative imports: 15 | import LIVR = require('../lib/LIVR'); 16 | type InferFromSchema = LIVR.InferFromSchema; 17 | 18 | // ============================================================================ 19 | // Define Schema 20 | // ============================================================================ 21 | 22 | const userSchema = { 23 | name: ['required', 'string'], 24 | email: ['required', 'email'], 25 | age: 'positive_integer', 26 | role: { one_of: ['admin', 'user', 'guest'] as const }, 27 | status: { eq: 'active' as const }, 28 | } as const; 29 | 30 | // Infer TypeScript type from schema 31 | type User = InferFromSchema; 32 | // Result: { 33 | // name: string; 34 | // email: string; 35 | // age?: number; 36 | // role?: 'admin' | 'user' | 'guest'; 37 | // status?: 'active'; 38 | // } 39 | 40 | // ============================================================================ 41 | // Create Validator and Validate Data 42 | // ============================================================================ 43 | 44 | const validator = new LIVR.Validator(userSchema); 45 | 46 | // Sample input (simulating data from API, form, etc.) 47 | const input: unknown = { 48 | name: 'John Doe', 49 | email: 'john@example.com', 50 | age: 30, 51 | role: 'admin', 52 | status: 'active', 53 | }; 54 | 55 | const validData = validator.validate(input); 56 | 57 | if (validData) { 58 | // validData is typed as User - hover to verify! 59 | console.log('Validation passed!'); 60 | console.log('Name:', validData.name); // string 61 | console.log('Email:', validData.email); // string 62 | console.log('Age:', validData.age); // number | undefined 63 | console.log('Role:', validData.role); // 'admin' | 'user' | 'guest' | undefined 64 | console.log('Status:', validData.status); // 'active' | undefined 65 | } else { 66 | console.log('Validation failed:', validator.getErrors()); 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livr", 3 | "version": "2.10.1", 4 | "description": "Lightweight validator supporting Language Independent Validation Rules Specification", 5 | "homepage": "https://github.com/koorchik/js-validator-livr", 6 | "author": { 7 | "name": "koorchik", 8 | "url": "https://github.com/koorchik" 9 | }, 10 | "license": "MIT", 11 | "main": "./lib/LIVR.js", 12 | "types": "types/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": "./types/index.d.ts", 16 | "require": "./lib/LIVR.js", 17 | "import": "./lib/LIVR.js" 18 | }, 19 | "./types/inference": { 20 | "types": "./types/inference.d.ts" 21 | } 22 | }, 23 | "scripts": { 24 | "test": "nyc ava && tsc --noEmit", 25 | "bench": "node benchmarks/bench.js", 26 | "build-sync:min": "webpack --entry ./scripts/browser_build_entry-sync.js --mode production -o ./dist/production/", 27 | "build-sync:debug": "webpack --entry ./scripts/browser_build_entry-sync.js --mode development -o ./dist/development/", 28 | "build-async:min": "webpack --entry ./scripts/browser_build_entry-async.js --mode production -o ./dist/production-async/", 29 | "build-async:debug": "webpack --entry ./scripts/browser_build_entry-async.js --mode development -o ./dist/development-async/", 30 | "build": "npm run build-sync:min && npm run build-sync:debug && npm run build-async:min && npm run build-async:debug" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/koorchik/js-validator-livr.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/koorchik/js-validator-livr/issues" 38 | }, 39 | "keywords": [ 40 | "validator", 41 | "validation", 42 | "livr", 43 | "schema", 44 | "sanitize" 45 | ], 46 | "devDependencies": { 47 | "ava": "^6.4.1", 48 | "benchmark": "^2.1.4", 49 | "nyc": "^17.1.0", 50 | "typescript": "^5.9.3", 51 | "webpack": "^5.75.0", 52 | "webpack-cli": "^4.10.0" 53 | }, 54 | "ava": { 55 | "files": [ 56 | "t/tests-sync/*.js", 57 | "t/tests-async/*.js" 58 | ] 59 | }, 60 | "nyc": { 61 | "check-coverage": true, 62 | "per-file": true, 63 | "lines": 80, 64 | "statements": 80, 65 | "functions": 80, 66 | "branches": 80, 67 | "exclude": [ 68 | "t/**/*.js" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /t/tests-async/01-test_suite.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const fs = require('fs'); 4 | const LIVR = require('../../async'); 5 | 6 | iterateTestData('test_suite/positive', data => { 7 | test(`Positive: ${data.name}`, async (t) => { 8 | const validator = new LIVR.AsyncValidator(data.rules); 9 | const output = await validator.validate(data.input); 10 | 11 | t.deepEqual(output, data.output, 'Output should contain correct data'); 12 | }); 13 | }); 14 | 15 | iterateTestData('test_suite/negative', data => { 16 | test(`Negative ${data.name}`, async (t) => { 17 | const validator = new LIVR.AsyncValidator(data.rules); 18 | 19 | try { 20 | const output = await validator.validate(data.input); 21 | } catch (errors) { 22 | t.deepEqual(errors, data.errors, 'Validator should contain errors'); 23 | } 24 | }); 25 | }); 26 | 27 | iterateTestData('test_suite/aliases_positive', data => { 28 | test(`Aliases positive: ${data.name}`, async (t) => { 29 | const validator = new LIVR.AsyncValidator(data.rules); 30 | 31 | data.aliases.forEach(alias => { 32 | validator.registerAliasedRule(alias); 33 | }); 34 | 35 | const output = await validator.validate(data.input); 36 | t.deepEqual(output, data.output, 'Output should contain correct data'); 37 | }); 38 | }); 39 | 40 | iterateTestData('test_suite/aliases_negative', data => { 41 | test(`Aliases negative: ${data.name}`, async (t) => { 42 | const validator = new LIVR.AsyncValidator(data.rules); 43 | 44 | data.aliases.forEach(alias => { 45 | validator.registerAliasedRule(alias); 46 | }); 47 | 48 | try { 49 | const output = await validator.validate(data.input); 50 | } catch (errors) { 51 | t.deepEqual(errors, data.errors, 'Validator should contain errors'); 52 | } 53 | }); 54 | }); 55 | 56 | function iterateTestData(path, cb) { 57 | const rootPath = __dirname + '/../' + path; 58 | console.log(`ITERATE: ${rootPath}`); 59 | const casesDirs = fs.readdirSync(rootPath); 60 | 61 | for (const caseDir of casesDirs) { 62 | const caseFiles = fs.readdirSync(rootPath + '/' + caseDir); 63 | const caseData = { name: caseDir }; 64 | 65 | for (const file of caseFiles) { 66 | const json = fs.readFileSync(rootPath + '/' + caseDir + '/' + file); 67 | 68 | caseData[file.replace(/\.json$/, '')] = JSON.parse(json); 69 | } 70 | 71 | cb(caseData); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /async.js: -------------------------------------------------------------------------------- 1 | const AsyncValidator = require('./lib/AsyncValidator'); 2 | const util = require('./lib/util'); 3 | 4 | const rules = { 5 | required: require('./lib/rules/common/required'), 6 | not_empty: require('./lib/rules/common/not_empty'), 7 | not_empty_list: require('./lib/rules/common/not_empty_list'), 8 | any_object: require('./lib/rules/common/any_object'), 9 | 10 | string: require('./lib/rules/string/string'), 11 | eq: require('./lib/rules/string/eq'), 12 | one_of: require('./lib/rules/string/one_of'), 13 | max_length: require('./lib/rules/string/max_length'), 14 | min_length: require('./lib/rules/string/min_length'), 15 | length_equal: require('./lib/rules/string/length_equal'), 16 | length_between: require('./lib/rules/string/length_between'), 17 | like: require('./lib/rules/string/like'), 18 | 19 | integer: require('./lib/rules/numeric/integer'), 20 | positive_integer: require('./lib/rules/numeric/positive_integer'), 21 | decimal: require('./lib/rules/numeric/decimal'), 22 | positive_decimal: require('./lib/rules/numeric/positive_decimal'), 23 | max_number: require('./lib/rules/numeric/max_number'), 24 | min_number: require('./lib/rules/numeric/min_number'), 25 | number_between: require('./lib/rules/numeric/number_between'), 26 | 27 | email: require('./lib/rules/special/email'), 28 | equal_to_field: require('./lib/rules/special/equal_to_field'), 29 | url: require('./lib/rules/special/url'), 30 | iso_date: require('./lib/rules/special/iso_date'), 31 | 32 | default: require('./lib/rules/modifiers/default'), 33 | trim: require('./lib/rules/modifiers/trim'), 34 | to_lc: require('./lib/rules/modifiers/to_lc'), 35 | to_uc: require('./lib/rules/modifiers/to_uc'), 36 | remove: require('./lib/rules/modifiers/remove'), 37 | leave_only: require('./lib/rules/modifiers/leave_only'), 38 | 39 | // We import here special version of async meta rules 40 | nested_object: require('./lib/rules/meta-async/nested_object'), 41 | variable_object: require('./lib/rules/meta-async/variable_object'), 42 | list_of: require('./lib/rules/meta-async/list_of'), 43 | list_of_objects: require('./lib/rules/meta-async/list_of_objects'), 44 | or: require('./lib/rules/meta-async/or'), 45 | list_of_different_objects: require('./lib/rules/meta-async/list_of_different_objects') 46 | }; 47 | 48 | AsyncValidator.registerDefaultRules(rules); 49 | 50 | module.exports = { AsyncValidator, rules, util }; 51 | -------------------------------------------------------------------------------- /t/tests-sync/01-test_suite.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const fs = require('fs'); 4 | const LIVR = require('../../lib/LIVR'); 5 | const util = require('util'); 6 | 7 | iterateTestData('test_suite/positive', data => { 8 | test(`Positive: ${data.name}`, t => { 9 | const validator = new LIVR.Validator(data.rules); 10 | const output = validator.validate(data.input); 11 | 12 | const errors = validator.getErrors(); 13 | t.true( 14 | !errors, 15 | 'Validator should contain no errors. The error was ' + util.inspect(errors) 16 | ); 17 | t.deepEqual(output, data.output, 'Output should contain correct data'); 18 | }); 19 | }); 20 | 21 | iterateTestData('test_suite/negative', data => { 22 | test(`Negative ${data.name}`, t => { 23 | const validator = new LIVR.Validator(data.rules); 24 | const output = validator.validate(data.input); 25 | 26 | t.true(!output, 'Output should be false'); 27 | t.deepEqual(validator.getErrors(), data.errors, 'Validator should contain errors'); 28 | }); 29 | }); 30 | 31 | iterateTestData('test_suite/aliases_positive', data => { 32 | test(`Aliases positive: ${data.name}`, t => { 33 | const validator = new LIVR.Validator(data.rules); 34 | 35 | data.aliases.forEach(alias => { 36 | validator.registerAliasedRule(alias); 37 | }); 38 | 39 | const output = validator.validate(data.input); 40 | 41 | t.true(!validator.getErrors(), 'Validator should contain no errors'); 42 | t.deepEqual(output, data.output, 'Output should contain correct data'); 43 | }); 44 | }); 45 | 46 | iterateTestData('test_suite/aliases_negative', data => { 47 | test(`Aliases negative: ${data.name}`, t => { 48 | const validator = new LIVR.Validator(data.rules); 49 | 50 | data.aliases.forEach(alias => { 51 | validator.registerAliasedRule(alias); 52 | }); 53 | 54 | const output = validator.validate(data.input); 55 | 56 | t.true(!output, 'Output should be false'); 57 | t.deepEqual(validator.getErrors(), data.errors, 'Validator should contain errors'); 58 | }); 59 | }); 60 | 61 | function iterateTestData(path, cb) { 62 | const rootPath = __dirname + '/../' + path; 63 | console.log(`ITERATE: ${rootPath}`); 64 | const casesDirs = fs.readdirSync(rootPath); 65 | 66 | for (const caseDir of casesDirs) { 67 | const caseFiles = fs.readdirSync(rootPath + '/' + caseDir); 68 | const caseData = { name: caseDir }; 69 | 70 | for (const file of caseFiles) { 71 | const json = fs.readFileSync(rootPath + '/' + caseDir + '/' + file); 72 | 73 | caseData[file.replace(/\.json$/, '')] = JSON.parse(json); 74 | } 75 | 76 | cb(caseData); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /t/test_suite/negative/28-variable_object/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "order_id": ["required", "positive_integer"], 3 | "product1": ["required", { "variable_object": [ 4 | "product_type", { 5 | "material": { 6 | "product_type": "required", 7 | "material_id": ["required", "positive_integer"], 8 | "quantity": ["required", {"min_number": 1} ], 9 | "warehouse_id": "positive_integer" 10 | }, 11 | "service": { 12 | "product_type": "required", 13 | "name": ["required", {"max_length": 10} ] 14 | } 15 | } 16 | ]}], 17 | 18 | "product2": ["required", { "variable_object": [ 19 | "product_type", { 20 | "material": { 21 | "product_type": "required", 22 | "material_id": ["required", "positive_integer"], 23 | "quantity": ["required", {"min_number": 1} ], 24 | "warehouse_id": "positive_integer" 25 | }, 26 | "service": { 27 | "product_type": "required", 28 | "name": ["required", {"max_length": 10} ] 29 | } 30 | } 31 | ]}], 32 | 33 | "product3": ["required", { "variable_object": [ 34 | "product_type", { 35 | "material": { 36 | "product_type": "required", 37 | "material_id": ["required", "positive_integer"], 38 | "quantity": ["required", {"min_number": 1} ], 39 | "warehouse_id": "positive_integer" 40 | }, 41 | "service": { 42 | "product_type": "required", 43 | "name": ["required", {"max_length": 10} ] 44 | } 45 | } 46 | ]}], 47 | 48 | "product4": ["required", { "variable_object": [ 49 | "product_type", { 50 | "material": { 51 | "product_type": "required", 52 | "material_id": ["required", "positive_integer"], 53 | "quantity": ["required", {"min_number": 1} ], 54 | "warehouse_id": "positive_integer" 55 | }, 56 | "service": { 57 | "product_type": "required", 58 | "name": ["required", {"max_length": 10} ] 59 | } 60 | } 61 | ]}], 62 | 63 | "product5": ["required", { "variable_object": [ 64 | "product_type", { 65 | "material": { 66 | "product_type": "required", 67 | "material_id": ["required", "positive_integer"], 68 | "quantity": ["required", {"min_number": 1} ], 69 | "warehouse_id": "positive_integer" 70 | }, 71 | "service": { 72 | "product_type": "required", 73 | "name": ["required", {"max_length": 10} ] 74 | } 75 | } 76 | ]}] 77 | } 78 | -------------------------------------------------------------------------------- /lib/AsyncValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('./util'); 4 | const BaseValidator = require('./BaseValidator'); 5 | 6 | class AsyncValidator extends BaseValidator { 7 | static buildAliasedRule(rules, errorCode) { 8 | if (!rules) throw 'Alias rules required'; 9 | 10 | const livr = { value: rules }; 11 | 12 | return ruleBuilders => { 13 | // ruleBuilders comes always as a last argument 14 | // we register them to support custom rules in aliases 15 | const validator = new AsyncValidator(livr).registerRules(ruleBuilders).prepare(); 16 | 17 | return async (value, undefined, outputArr) => { 18 | try { 19 | const result = await validator.validate({ value }); 20 | outputArr.push(result.value); 21 | return; 22 | } catch (errors) { 23 | return errorCode || errors.value; 24 | } 25 | }; 26 | }; 27 | } 28 | 29 | async validate(data) { 30 | if (!this.isPrepared) this.prepare(); 31 | 32 | if (!util.isObject(data)) { 33 | this.errors = 'FORMAT_ERROR'; 34 | return Promise.reject('FORMAT_ERROR'); 35 | } 36 | 37 | if (this.options.autoTrim) { 38 | data = this._autoTrim(data); 39 | } 40 | 41 | const errors = {}; 42 | const result = {}; 43 | 44 | await Promise.all( 45 | Object.keys(this.validators).map( 46 | fieldName => this.validateField(fieldName, data, result, errors) 47 | ) 48 | ); 49 | 50 | if (util.isEmptyObject(errors)) { 51 | this.errors = null; 52 | return result; 53 | } else { 54 | this.errors = errors; 55 | return Promise.reject(errors); 56 | } 57 | } 58 | 59 | async validateField(fieldName, data, result, errors) { 60 | const validators = this.validators[fieldName]; 61 | if (!validators || !validators.length) return; 62 | 63 | const value = data[fieldName]; 64 | 65 | for (const validator of validators) { 66 | const fieldResultArr = []; 67 | 68 | const errCode = await validator( 69 | result.hasOwnProperty(fieldName) ? result[fieldName] : value, 70 | data, 71 | fieldResultArr 72 | ); 73 | 74 | if (errCode) { 75 | errors[fieldName] = errCode; 76 | break; 77 | } else if (fieldResultArr.length) { 78 | result[fieldName] = fieldResultArr[0]; 79 | } else if (data.hasOwnProperty(fieldName) && !result.hasOwnProperty(fieldName)) { 80 | result[fieldName] = value; 81 | } 82 | } 83 | } 84 | 85 | } 86 | 87 | module.exports = AsyncValidator; 88 | -------------------------------------------------------------------------------- /lib/Validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseValidator = require('./BaseValidator'); 4 | const util = require('./util'); 5 | 6 | class Validator extends BaseValidator { 7 | constructor(...args) { 8 | super(...args); 9 | this.errors = null; 10 | } 11 | 12 | static buildAliasedRule(rules, errorCode) { 13 | if (!rules) throw 'Alias rules required'; 14 | 15 | const livr = { value: rules }; 16 | 17 | return ruleBuilders => { 18 | // ruleBuilders comes always as a last argument 19 | // we register them to support custom rules in aliases 20 | const validator = new Validator(livr).registerRules(ruleBuilders).prepare(); 21 | 22 | return (value, undefined, outputArr) => { 23 | const result = validator.validate({ value }); 24 | 25 | if (result) { 26 | outputArr.push(result.value); 27 | return; 28 | } else { 29 | return errorCode || validator.getErrors().value; 30 | } 31 | }; 32 | }; 33 | } 34 | 35 | validate(data) { 36 | if (!this.isPrepared) this.prepare(); 37 | 38 | if (!util.isObject(data)) { 39 | this.errors = 'FORMAT_ERROR'; 40 | return; 41 | } 42 | 43 | if (this.options.autoTrim) { 44 | data = this._autoTrim(data); 45 | } 46 | 47 | const errors = {}; 48 | const result = {}; 49 | 50 | for (const fieldName in this.validators) { 51 | const validators = this.validators[fieldName]; 52 | if (!validators || !validators.length) continue; 53 | 54 | const value = data[fieldName]; 55 | let resultFieldValue; 56 | let resultFieldValueIsSet = false; 57 | 58 | for (const validator of validators) { 59 | const updatedFieldValueArr = []; 60 | 61 | const errCode = validator( 62 | resultFieldValueIsSet ? resultFieldValue : value, 63 | data, 64 | updatedFieldValueArr 65 | ); 66 | 67 | if (errCode) { 68 | errors[fieldName] = errCode; 69 | break; 70 | } else if (updatedFieldValueArr.length) { 71 | resultFieldValue = updatedFieldValueArr[0]; 72 | resultFieldValueIsSet = true; 73 | } else if (!resultFieldValueIsSet && data.hasOwnProperty(fieldName) ) { 74 | resultFieldValue = value; 75 | resultFieldValueIsSet = true; 76 | } 77 | } 78 | 79 | if (resultFieldValueIsSet) { 80 | result[fieldName] = resultFieldValue; 81 | } 82 | } 83 | 84 | if (util.isEmptyObject(errors)) { 85 | this.errors = null; 86 | return result; 87 | } else { 88 | this.errors = errors; 89 | return false; 90 | } 91 | } 92 | 93 | getErrors() { 94 | return this.errors; 95 | } 96 | } 97 | 98 | module.exports = Validator; 99 | -------------------------------------------------------------------------------- /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 (Language Independent Validation Rules) is a JavaScript validator implementing the LIVR specification. It provides declarative, language-independent validation rules for data validation. 8 | 9 | ## Commands 10 | 11 | ```bash 12 | # Run all tests (runtime tests with coverage + TypeScript type checks) 13 | npm test 14 | 15 | # Run runtime tests only (using AVA test runner) 16 | npx ava 17 | 18 | # Run a specific test file 19 | npx ava t/tests-sync/01-test_suite.js 20 | npx ava t/tests-async/01-test_suite.js 21 | 22 | # Type check only (verify TypeScript definitions and type inference) 23 | npx tsc --noEmit 24 | 25 | # Build browser bundles (sync and async, dev and production) 26 | npm run build 27 | ``` 28 | 29 | ## Architecture 30 | 31 | ### Core Classes (lib/) 32 | 33 | - **BaseValidator.js** - Abstract base class containing shared validation logic: rule parsing, validator building, auto-trim functionality, and static methods for registering default rules 34 | - **Validator.js** - Synchronous validator extending BaseValidator. Returns `false` on validation failure, validated data on success 35 | - **AsyncValidator.js** - Asynchronous validator extending BaseValidator. Validates fields in parallel but processes rules sequentially per field. Rejects with errors on failure 36 | - **LIVR.js** - Main entry point for sync validator; registers all default rules and exports `{ Validator, rules, util }` 37 | - **async.js** (root) - Entry point for async validator with async-specific meta rules 38 | 39 | ### Rule Structure (lib/rules/) 40 | 41 | Rules are organized by category: 42 | - `common/` - required, not_empty, not_empty_list, any_object 43 | - `string/` - eq, one_of, min_length, max_length, length_equal, length_between, like, string 44 | - `numeric/` - integer, positive_integer, decimal, positive_decimal, min_number, max_number, number_between 45 | - `special/` - email, url, equal_to_field, iso_date 46 | - `modifiers/` - trim, to_lc, to_uc, default, remove, leave_only 47 | - `meta/` - nested_object, variable_object, list_of, list_of_objects, list_of_different_objects, or 48 | - `meta-async/` - Async versions of meta rules for AsyncValidator 49 | 50 | ### Rule Builder Pattern 51 | 52 | Each rule is a factory function that receives rule arguments and returns a validator function: 53 | ```javascript 54 | function ruleName(arg1, arg2, ruleBuilders) { 55 | return (value, allValues, outputArr) => { 56 | // Return error code string on failure, undefined on success 57 | // Push to outputArr to modify the output value 58 | }; 59 | } 60 | ``` 61 | 62 | ### Test Structure (t/) 63 | 64 | - Tests use AVA framework 65 | - `t/tests-sync/` - Sync validator tests 66 | - `t/tests-async/` - Async validator tests 67 | - `t/test_suite/` - JSON-based test cases (positive, negative, aliases_positive, aliases_negative) 68 | - Each test case is a directory containing `rules.json`, `input.json`, and `output.json` (positive) or `errors.json` (negative) 69 | - Alias tests also include `aliases.json` 70 | - `t/types-test.ts` - Compile-time type inference tests (verified via `tsc --noEmit`) 71 | 72 | ### TypeScript Types (types/) 73 | 74 | - `types/index.d.ts` - Main type declarations for Validator class and exports 75 | - `types/inference.d.ts` - Type inference engine for `InferFromSchema` functionality 76 | - Each rule in `lib/rules/` has a corresponding `.d.ts` file that registers its type in `RuleTypeRegistry` 77 | 78 | ### Utilities (lib/util.js) 79 | 80 | Helper functions: `isPrimitiveValue`, `looksLikeNumber`, `isObject`, `isEmptyObject`, `escapeRegExp`, `isNoValue`, `camelize` 81 | 82 | ## Key Patterns 83 | 84 | - Rule names support both underscore (`min_length`) and camelCase (`minLength`) - automatic camelization on registration 85 | - Validators are reusable - construct once, validate many inputs 86 | - `prepare()` compiles rules on first validation or can be called manually for warmup 87 | - Empty values (`undefined`, `null`, `''`) skip validation unless using `required` rule 88 | - Sync validator returns `false` on failure; async validator throws/rejects with errors object 89 | - Use `import LIVR from 'livr'` for sync, `import LIVR from 'livr/async'` for async 90 | -------------------------------------------------------------------------------- /lib/BaseValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('./util'); 4 | 5 | const DEFAULT_VALIDATOR_BUILDERS = {}; 6 | const DEFAULT_OPTIONS = { 7 | autoTrim: false, 8 | }; 9 | 10 | class BaseValidator { 11 | constructor(livrRules, arg) { 12 | let options = {}; 13 | if (util.isObject(arg)) { 14 | options = arg; 15 | } else if (arg !== null && arg !== undefined) { 16 | options.autoTrim = arg; 17 | } 18 | 19 | this.options = { ...DEFAULT_OPTIONS, ...options }; 20 | this.isPrepared = false; 21 | this.livrRules = livrRules; 22 | this.validators = {}; 23 | this.validatorBuilders = {}; 24 | } 25 | 26 | static getDefaultRules() { 27 | return DEFAULT_VALIDATOR_BUILDERS; 28 | } 29 | 30 | static registerAliasedDefaultRule(alias) { 31 | if (!alias.name) throw new Error('Alias name required'); 32 | 33 | DEFAULT_VALIDATOR_BUILDERS[alias.name] = this.buildAliasedRule(alias.rules, alias.error); 34 | 35 | const camelizedName = util.camelize(alias.name); 36 | if (DEFAULT_VALIDATOR_BUILDERS[camelizedName]) { 37 | DEFAULT_VALIDATOR_BUILDERS[camelizedName] = DEFAULT_VALIDATOR_BUILDERS[alias.name]; 38 | } 39 | } 40 | 41 | static registerDefaultRules(rules) { 42 | for (const ruleName in rules) { 43 | DEFAULT_VALIDATOR_BUILDERS[ruleName] = rules[ruleName]; 44 | 45 | const camelizedName = util.camelize(ruleName); 46 | if (!DEFAULT_VALIDATOR_BUILDERS[camelizedName]) { 47 | DEFAULT_VALIDATOR_BUILDERS[camelizedName] = DEFAULT_VALIDATOR_BUILDERS[ruleName]; 48 | } 49 | } 50 | } 51 | 52 | static defaultAutoTrim(isAutoTrim) { 53 | DEFAULT_OPTIONS.autoTrim = !!isAutoTrim; 54 | } 55 | 56 | prepare() { 57 | const allRules = this.livrRules; 58 | 59 | for (const field in allRules) { 60 | let fieldRules = allRules[field]; 61 | 62 | if (!Array.isArray(fieldRules)) { 63 | fieldRules = [fieldRules]; 64 | } 65 | 66 | const validators = []; 67 | 68 | for (const fieldRule of fieldRules) { 69 | const parsed = this._parseRule(fieldRule); 70 | validators.push(this._buildValidator(parsed.name, parsed.args)); 71 | } 72 | 73 | this.validators[field] = validators; 74 | } 75 | 76 | this.isPrepared = true; 77 | return this; 78 | } 79 | 80 | registerRules(rules) { 81 | for (const ruleName in rules) { 82 | this.validatorBuilders[ruleName] = rules[ruleName]; 83 | } 84 | 85 | return this; 86 | } 87 | 88 | registerAliasedRule(alias) { 89 | if (!alias.name) throw new Error('Alias name required'); 90 | 91 | this.validatorBuilders[alias.name] = this.constructor.buildAliasedRule( 92 | alias.rules, 93 | alias.error 94 | ); 95 | 96 | return this; 97 | } 98 | 99 | getRules() { 100 | return this.validatorBuilders; 101 | } 102 | 103 | _parseRule(livrRule) { 104 | let name; 105 | let args; 106 | 107 | if (util.isObject(livrRule)) { 108 | name = Object.keys(livrRule)[0]; 109 | args = livrRule[name]; 110 | 111 | if (!Array.isArray(args)) args = [args]; 112 | } else { 113 | name = livrRule; 114 | args = []; 115 | } 116 | 117 | return { name, args }; 118 | } 119 | 120 | _buildValidator(name, args) { 121 | const validatorBuilder = this.validatorBuilders[name] || DEFAULT_VALIDATOR_BUILDERS[name]; 122 | 123 | if (!validatorBuilder) { 124 | throw new Error(`Rule [${name}] not registered`); 125 | } 126 | 127 | return validatorBuilder(...args, this.getRules()); 128 | } 129 | 130 | _autoTrim(data) { 131 | if (data === null || data === undefined) { 132 | return data; 133 | } 134 | 135 | const dataType = typeof data; 136 | 137 | if (dataType === 'string') { 138 | return data.trim(); 139 | } 140 | 141 | if (dataType !== 'object') { 142 | return data; 143 | } 144 | 145 | if (Array.isArray(data)) { 146 | const len = data.length; 147 | const trimmedData = new Array(len); 148 | for (let i = 0; i < len; i++) { 149 | trimmedData[i] = this._autoTrim(data[i]); 150 | } 151 | return trimmedData; 152 | } 153 | 154 | if (data.constructor === Object) { 155 | const trimmedData = {}; 156 | const keys = Object.keys(data); 157 | for (let i = 0; i < keys.length; i++) { 158 | const key = keys[i]; 159 | trimmedData[key] = this._autoTrim(data[key]); 160 | } 161 | return trimmedData; 162 | } 163 | 164 | return data; 165 | } 166 | } 167 | 168 | module.exports = BaseValidator; 169 | -------------------------------------------------------------------------------- /benchmarks/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Benchmark = require('benchmark'); 4 | const LIVR = require('../lib/LIVR'); 5 | 6 | const suite = new Benchmark.Suite(); 7 | 8 | // Scenario 1: Simple flat form validation (primary focus) 9 | const simpleSchema = { 10 | name: 'required', 11 | email: 'email', 12 | age: 'positive_integer', 13 | phone: { max_length: 20 }, 14 | website: 'url', 15 | bio: { max_length: 1000 } 16 | }; 17 | const simpleValidator = new LIVR.Validator(simpleSchema); 18 | simpleValidator.prepare(); 19 | 20 | const simpleData = { 21 | name: 'John Doe', 22 | email: 'john@example.com', 23 | age: 25, 24 | phone: '555-1234', 25 | website: 'https://example.com', 26 | bio: 'A software developer' 27 | }; 28 | 29 | // Scenario 2: Multiple rules per field 30 | const multiRuleSchema = { 31 | username: ['required', { min_length: 3 }, { max_length: 20 }], 32 | email: ['required', 'email'], 33 | password: ['required', { min_length: 8 }], 34 | age: ['required', 'positive_integer', { min_number: 18 }] 35 | }; 36 | const multiRuleValidator = new LIVR.Validator(multiRuleSchema); 37 | multiRuleValidator.prepare(); 38 | 39 | const multiRuleData = { 40 | username: 'johndoe', 41 | email: 'john@example.com', 42 | password: 'secretpass123', 43 | age: 25 44 | }; 45 | 46 | // Scenario 3: String-heavy validation (trim, to_lc, length checks) 47 | const stringSchema = { 48 | firstName: ['required', 'trim', { min_length: 2 }, { max_length: 50 }], 49 | lastName: ['required', 'trim', { min_length: 2 }, { max_length: 50 }], 50 | email: ['required', 'trim', 'to_lc', 'email'], 51 | phone: ['trim', { max_length: 20 }], 52 | address: ['trim', { max_length: 200 }] 53 | }; 54 | const stringValidator = new LIVR.Validator(stringSchema); 55 | stringValidator.prepare(); 56 | 57 | const stringData = { 58 | firstName: ' John ', 59 | lastName: ' Doe ', 60 | email: ' JOHN@EXAMPLE.COM ', 61 | phone: ' 555-1234 ', 62 | address: ' 123 Main St ' 63 | }; 64 | 65 | // Scenario 4: Numeric validation 66 | const numericSchema = { 67 | age: 'positive_integer', 68 | price: 'decimal', 69 | quantity: ['integer', { min_number: 0 }, { max_number: 1000 }], 70 | rating: ['decimal', { min_number: 0 }, { max_number: 5 }], 71 | score: { number_between: [0, 100] } 72 | }; 73 | const numericValidator = new LIVR.Validator(numericSchema); 74 | numericValidator.prepare(); 75 | 76 | const numericData = { 77 | age: 25, 78 | price: '19.99', 79 | quantity: 5, 80 | rating: '4.5', 81 | score: 85 82 | }; 83 | 84 | // Scenario 5: Date validation (iso_date) 85 | const dateSchema = { 86 | birthDate: ['required', 'iso_date'], 87 | startDate: 'iso_date', 88 | endDate: 'iso_date' 89 | }; 90 | const dateValidator = new LIVR.Validator(dateSchema); 91 | dateValidator.prepare(); 92 | 93 | const dateData = { 94 | birthDate: '1990-05-15', 95 | startDate: '2024-01-01', 96 | endDate: '2024-12-31' 97 | }; 98 | 99 | // Scenario 6: Larger form (15 fields) 100 | const largeFormSchema = { 101 | firstName: 'required', 102 | lastName: 'required', 103 | email: ['required', 'email'], 104 | phone: { max_length: 20 }, 105 | age: 'positive_integer', 106 | address1: { max_length: 100 }, 107 | address2: { max_length: 100 }, 108 | city: 'required', 109 | state: { max_length: 50 }, 110 | zip: { max_length: 10 }, 111 | country: 'required', 112 | website: 'url', 113 | company: { max_length: 100 }, 114 | title: { max_length: 100 }, 115 | bio: { max_length: 1000 } 116 | }; 117 | const largeFormValidator = new LIVR.Validator(largeFormSchema); 118 | largeFormValidator.prepare(); 119 | 120 | const largeFormData = { 121 | firstName: 'John', 122 | lastName: 'Doe', 123 | email: 'john@example.com', 124 | phone: '555-1234', 125 | age: 30, 126 | address1: '123 Main St', 127 | address2: 'Apt 4B', 128 | city: 'New York', 129 | state: 'NY', 130 | zip: '10001', 131 | country: 'USA', 132 | website: 'https://johndoe.com', 133 | company: 'Acme Corp', 134 | title: 'Software Engineer', 135 | bio: 'Experienced developer' 136 | }; 137 | 138 | // Scenario 7: Validation with errors (tests error path) 139 | const errorSchema = { 140 | email: ['required', 'email'], 141 | age: ['required', 'positive_integer'] 142 | }; 143 | const errorValidator = new LIVR.Validator(errorSchema); 144 | errorValidator.prepare(); 145 | 146 | const errorData = { 147 | email: 'not-an-email', 148 | age: -5 149 | }; 150 | 151 | // Scenario 8: Enum/choice validation (one_of, eq) 152 | const enumSchema = { 153 | status: { one_of: ['pending', 'active', 'suspended', 'deleted'] }, 154 | role: { one_of: ['admin', 'user', 'guest'] }, 155 | priority: { one_of: [1, 2, 3, 4, 5] }, 156 | type: { eq: 'standard' }, 157 | category: { one_of: ['electronics', 'clothing', 'food', 'other'] } 158 | }; 159 | const enumValidator = new LIVR.Validator(enumSchema); 160 | enumValidator.prepare(); 161 | 162 | const enumData = { 163 | status: 'active', 164 | role: 'user', 165 | priority: 3, 166 | type: 'standard', 167 | category: 'electronics' 168 | }; 169 | 170 | console.log('LIVR Validator Benchmark Suite'); 171 | console.log('==============================\n'); 172 | 173 | suite 174 | .add('simple-form (6 fields, basic rules)', () => { 175 | simpleValidator.validate(simpleData); 176 | }) 177 | .add('multi-rule (4 fields, 3-4 rules each)', () => { 178 | multiRuleValidator.validate(multiRuleData); 179 | }) 180 | .add('string-heavy (5 fields, trim/length)', () => { 181 | stringValidator.validate(stringData); 182 | }) 183 | .add('numeric (5 fields, number rules)', () => { 184 | numericValidator.validate(numericData); 185 | }) 186 | .add('date-validation (3 iso_date fields)', () => { 187 | dateValidator.validate(dateData); 188 | }) 189 | .add('large-form (15 fields)', () => { 190 | largeFormValidator.validate(largeFormData); 191 | }) 192 | .add('error-path (validation failure)', () => { 193 | errorValidator.validate(errorData); 194 | }) 195 | .add('enum-validation (5 fields, one_of/eq)', () => { 196 | enumValidator.validate(enumData); 197 | }) 198 | .on('cycle', event => { 199 | console.log(String(event.target)); 200 | }) 201 | .on('complete', function() { 202 | console.log('\n=============================='); 203 | console.log('Benchmark complete'); 204 | }) 205 | .run({ async: true }); 206 | --------------------------------------------------------------------------------