alchemymvc
Version:
MVC framework for Node.js
512 lines (425 loc) • 10.7 kB
JavaScript
/**
* Path definition parser
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.3.0
*/
const PathDefinition = Function.inherits('Alchemy.Base', function PathDefinition(path, options) {
// Store the original path
this.path = path;
this.source = path;
// Store the key tokens
this.key_tokens = [];
// Store the param definitions
this.param_definitions = null;
// Store the key names
this.keys = [];
// Path options
this.options = options || {};
// The prefix of this path
this.prefix = this.options.prefix;
// Get the tokens
let tokens = this.parse(path);
if (!this.regex) {
let i;
// Compile the regex
this.regex = this.tokensToRegexp(tokens, this.key_tokens);
for (i = 0; i < this.key_tokens.length; i++) {
this.keys[i] = this.key_tokens[i].name;
}
this.param_definitions = Classes.Alchemy.PathParamDefinition.from(this.key_tokens);
}
});
/**
* The main path matching regexp utility
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.1.0
*/
PathDefinition.setStatic('path_regexp', RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// "/{test(\\d+)}?" => ["{test(\\d+)}?", undefined, undefined, "test", "\\d+", undefined, "?"]
// "/route(\\d+)" => ["(\\d+)", undefined, undefined, undefined, undefined, "\\d+", undefined]
// "/{[ObjectId]test}" => ["{[ObjectId]test}", undefined, "ObjectId", "test", undefined, undefined, undefined]
'(?:{(?:\\[((?:\\w+\\.?)+)\\])?(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))}?([+*?])?'
].join('|'), 'g'));
/**
* Type definition testers
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.0.0
*/
PathDefinition.setStatic('typedefs', {});
/**
* Register a type (like ObjectId or Number)
*
* @param {string} name
* @param {Function} fnc
*/
PathDefinition.setStatic(function registerType(name, fnc) {
if (typeof name == 'function') {
fnc = name;
name = fnc.name;
}
this.typedefs[name] = fnc;
});
/**
* Escape a regular expression string.
*
* @param {string} group
*
* @return {string}
*/
PathDefinition.setStatic(function escapeGroup(group) {
return group.replace(/([=!:$/()])/g, '\\$1')
});
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} str
*
* @return {string}
*/
PathDefinition.setStatic(function escapeString(str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
});
/**
* Does this path use any type class checks?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.0
* @version 1.3.0
*/
PathDefinition.enforceProperty(function uses_type_class_checks(new_value) {
if (new_value == null) {
new_value = false;
if (this.param_definitions?.length) {
for (let def of this.param_definitions) {
def.parseTypeDefinition();
if (def.type_class_constructor) {
new_value = true;
break;
}
}
}
}
return new_value;
});
/**
* Get the complete section identifier
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.1.0
*/
PathDefinition.setMethod(function parse(str, options) {
var default_delimiter = (options && options.delimiter) || '/',
path_escaped = false,
delimiters = (options && options.delimiters) || './',
delimiter,
modifier,
optional,
partial,
pattern,
capture,
escaped,
typedef,
repeat,
offset,
tokens = [],
index = 0,
token,
group,
match,
name,
next,
prev,
path = '',
key = 0,
res,
k;
if (RegExp.isRegExp(str)) {
this.regex = str;
this.from_regex = true;
return;
}
while ((res = PathDefinition.path_regexp.exec(str)) != null) {
match = res[0];
escaped = res[1];
typedef = res[2];
offset = res.index;
// Add everything from the last found index to this found one
path += str.slice(index, offset);
// Increase the index with the matched length
index = offset + match.length;
if (escaped) {
path += escaped[1];
path_escaped = true;
continue;
}
prev = '';
next = str[index];
name = res[3];
capture = res[4];
group = res[5];
modifier = res[6];
if (!path_escaped && path.length) {
k = path.length - 1;
if (delimiters.indexOf(path[k]) > -1) {
prev = path[k];
path = path.slice(0, k);
}
}
if (path) {
tokens.push(path);
path = '';
path_escaped = false;
}
partial = prev !== '' && next !== undefined && next !== prev;
repeat = modifier === '+' || modifier === '*';
optional = modifier === '?' || modifier === '*';
delimiter = prev || default_delimiter;
pattern = capture || group;
if (pattern) {
pattern = PathDefinition.escapeGroup(pattern);
} else {
pattern = '[^' + PathDefinition.escapeString(delimiter) + ']+?';
}
if (typedef && typedef.indexOf('.') > -1) {
typedef = typedef.split('.');
}
token = {
name : name || key++,
prefix : prev,
delimiter : delimiter,
optional : optional,
typedef : typedef,
repeat : repeat,
partial : partial,
pattern : pattern
};
tokens.push(token);
}
// Push any remaining characters
if (path || index < str.length) {
tokens.push(path + str.substr(index));
}
return tokens;
});
/**
* Convert tokens to regular expression
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.0.0
*
* @param {Array} tokens
* @param {Array} keys The keys will be stored in here
* @param {Object} options
*/
PathDefinition.setMethod(function tokensToRegexp(tokens, keys, options) {
var is_end_delimited = false,
delimiters,
delimiter,
capture,
prefix,
strict,
route = '',
token,
end,
i;
options = options || {};
delimiters = options.delimiters || './';
delimiter = PathDefinition.escapeString(options.delimiter || '/');
if (options.end != null) {
end = options.end;
} else if (this.options.end != null) {
end = this.options.end;
} else {
end = false;
}
// Iterate over the tokens and create the regexp
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
if (typeof token == 'string') {
route += PathDefinition.escapeString(token);
if (i === tokens.length - 1 && delimiter.indexOf(token[token.length - 1]) > -1) {
is_end_delimited = true;
} else {
is_end_delimited = false;
}
} else {
prefix = PathDefinition.escapeString(token.prefix);
if (token.repeat) {
capture = '(?:' + token.pattern + ')(?:' + prefix + '(?:' + token.pattern + '))*';
} else {
capture = token.pattern;
}
if (keys) {
keys.push(token);
}
if (token.optional) {
if (token.partial) {
route += prefix + '(' + capture + ')?';
} else {
route += '(?:' + prefix + '(' + capture + '))?';
}
} else {
route += prefix + '(' + capture + ')';
}
}
}
if (end) {
if (!strict) {
route += '(?:' + delimiter + ')?';
}
route += '$';
} else {
if (!strict) {
route += '(?:' + delimiter + '(?=' + '$' + '))?';
}
if (!is_end_delimited) {
route += '(?=' + delimiter + '|' + '$' + ')';
}
}
return RegExp('^' + route, 'i');
});
/**
* Get parameters object
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.1.0
*
* @param {Array} values
* @param {string} info_type
*
* @return {Object}
*/
PathDefinition.setMethod(function getParametersObject(values, info_type) {
var result = {},
key,
i;
// Paths made from a regex have no named parameters, just store them under their index
if (this.from_regex) {
for (i = 0; i < values.length; i++) {
result[i] = values[i].value;
}
return result;
}
let all_info = info_type == 'all_info',
original_value = info_type == 'original_value';
for (i = 0; i < this.keys.length; i++) {
key = this.keys[i];
if (all_info) {
result[key] = values[i];
} else if (original_value) {
result[key] = values[i].original_value;
} else {
result[key] = values[i].value;
}
}
return result;
});
/**
* Test the given string
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.3.0
*
* @param {string} path
*
* @return {Array|Pledge}
*/
PathDefinition.setMethod(function test(path, conduit) {
const values = this.regex.exec(path);
if (!values) {
return null;
}
// Remove the first part of the value, it's just the match
values.shift();
let result = new Array(values.length),
do_await = false;
// Iterate over the rest of the found values
for (let index = 0; index < values.length; index++) {
let value = RURL.decodeUriSegment(values[index]);
let param_def = this.param_definitions?.[index];
// There will be no param definition for regex-style matching
if (!param_def) {
let token = this.key_tokens[index] || {};
result[index] = {
name : token.name,
value : value,
original_value : value
};
continue;
}
let entry = param_def.parsePathValue(value, conduit);
// Return early when one of the checks fail
if (!entry) {
return null;
}
if (Pledge.isThenable(entry)) {
do_await = true;
} else if (entry.rejected) {
return null;
}
result[index] = entry;
}
if (do_await) {
return Function.parallel(result, (err, result) => {
if (err) {
return null;
}
let i;
for (i = 0; i < result.length; i++) {
if (result[i]?.rejected) {
return null;
}
}
return result;
});
}
return result;
});
/**
* Make sure it's a valid ObjectId string
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.0.0
*
* @param {string} value
*
* @return {string}
*/
PathDefinition.registerType(function ObjectId(value) {
if (value.isObjectId()) {
return value;
}
});
/**
* Make sure it's a number
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.0.0
*
* @param {string} value
*
* @return {number}
*/
PathDefinition.registerType(function Number(value) {
if (Classes.Number.isNumeric(value)) {
return Classes.Number(value);
}
});