indicative-parser
Version:
Schema parser for Indicative
242 lines (241 loc) • 7.18 kB
JavaScript
;
/*
* indicative-parser
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const haye_pipe_1 = __importDefault(require("haye/dist/haye-pipe"));
const haye_array_presenter_1 = __importDefault(require("haye/dist/haye-array-presenter"));
function toCamelCase(ruleName) {
return ruleName.replace(/_(\w)/g, (_match, group) => group.toUpperCase());
}
/**
* Updates rules on the given node. If node is missing, then a literal node is
* created automatically. Literal nodes can later transform into `object` and
* `array` nodes.
*/
function setLiteral(source, key, rules) {
const item = (source[key] || { type: 'literal' });
item.rules = rules;
source[key] = item;
return item;
}
/**
* Creates/updates literal node to an object node. Since `object` node
* properties are different from `literal` node, we need to set those
* properties (if missing).
*
* If node already exists and is an array node, then this method will raise an
* exception
*/
function setObject(source, key) {
if (source[key] && source[key].type === 'array') {
throw new Error(`cannot reshape ${key} array to an object`);
}
const item = (source[key] || { rules: [] });
item.type = 'object';
item.children = item.children || {};
source[key] = item;
return item;
}
/**
* Creates/updates literal node to an array node. Since `array` node
* properties are different from `literal` node, we need to set those
* properties (if missing).
*
* If node already exists and is an object node, then this method will raise an
* exception
*/
function setArray(source, key, index) {
if (source[key] && source[key].type === 'object') {
throw new Error(`cannot reshape ${key} object to an array`);
}
const item = (source[key] || { rules: [] });
item.each = item.each || {};
item.each[index] = item.each[index] || { children: {}, rules: [] };
item.type = 'array';
source[key] = item;
return item;
}
/**
* Parses field tokens recursively to a [[ParsedSchema]] tree
*/
function parseFieldForRules(tokens, rules, out, index = 0) {
const token = tokens[index++];
/**
* Finding if we are on the last item. Last item defines
* the rules for the current node inside the tree
*/
const isLast = tokens.length === index;
/**
* Indexed array have `digits` like `users.0.username`
*/
const isIndexedArray = /^\d+$/.test(tokens[index]);
/**
* Is upcoming token an array
*/
const isArray = tokens[index] === '*' || isIndexedArray;
/**
* Last item was marked as array, since current token is a `*`
* or has defined index
*/
if (token === '*' || /^\d+$/.test(token)) {
/**
* Last item must update rules for each item for the array
*/
if (isLast) {
out.each[token].rules = rules;
return;
}
/**
* Nested arrays
*/
if (isArray) {
/**
* The code after the error works fine. However, in order to support
* 2d arrays, we need to implement them inside the declarative
* schema and compiler as well.
*
* For now, it's okay to skip this feature and later work on it
* across all the modules.
*/
throw new Error('2d arrays are currently not supported');
// const item = setArray(
// (out as SchemaNodeArray).each[token].children,
// token,
// isIndexedArray ? tokens[index] : '*',
// )
// return parseFieldForRules(tokens, rules, item, index)
}
/**
* Otherwise continue recursion
*/
return parseFieldForRules(tokens, rules, out.each[token].children, index);
}
/**
* Last item in the list of tokens. we must
* patch the rules here.
*/
if (isLast) {
setLiteral(out, token, rules);
return;
}
/**
* Current item as an array
*/
if (isArray) {
const item = setArray(out, token, isIndexedArray ? tokens[index] : '*');
return parseFieldForRules(tokens, rules, item, index);
}
/**
* Falling back to object
*/
const item = setObject(out, token);
return parseFieldForRules(tokens, rules, item.children, index);
}
/**
* Parses the schema object to a tree of parsed schema. The
* output is optimized for executing validation rules.
*
* @example
* ```
* parser({
* 'users.*.username': 'required'
* })
*
* // output
*
* {
* users: {
* type: 'array',
* rules: [],
* each: {
* '*': {
* rules: [],
* children: {
* username: {
* type: 'literal',
* rules: [{
* name: 'required',
* args: []
* }]
* }
* }
* }
* }
* }
* }
* ```
*/
function rulesParser(schema) {
return Object
.keys(schema)
.reduce((result, field) => {
const rules = schema[field];
let parsedRules = [];
if (!rules) {
throw new Error(`make sure to define rules for ${field}`);
}
if (typeof (rules) === 'string') {
parsedRules = new haye_pipe_1.default(rules, new haye_array_presenter_1.default()).map((rule) => {
return { name: toCamelCase(rule.name), args: rule.args };
});
}
else {
parsedRules = rules;
}
parseFieldForRules(field.split('.'), parsedRules, result);
return result;
}, {});
}
exports.rulesParser = rulesParser;
/**
* Parses an object of messages to [[ParsedMessages]] list. The messages list
* is simpler than rules tree, since compiler can use the schema tree to find
* the appropriate messages from the flat list of messages.
*
* @example
* ```
* parser({
* 'users.*.username.required': 'Username is required'
* })
*
* // output
*
* {
* fields: {
* 'users.*.username': {
* required: 'Username is required'
* }
* },
* rules: {},
* }
*/
function messagesParser(schema) {
return Object
.keys(schema)
.reduce((result, field) => {
const message = schema[field];
const tokens = field.split('.');
const rule = toCamelCase(tokens.pop());
/**
* If token length is 1, then it is a plain rule vs `field.rule`
*/
if (!tokens.length) {
result.rules[rule] = message;
return result;
}
const qualifiedName = tokens.join('.');
result.fields[qualifiedName] = result.fields[qualifiedName] || {};
result.fields[qualifiedName][rule] = message;
return result;
}, { fields: {}, rules: {} });
}
exports.messagesParser = messagesParser;