'use strict';
const re = /.*/;
class Router {
constructor(options={}){
this.listeners = [];
this.options = {
name:"undefined123",
// prefix: "!",
...options
};
this.parent = this;
this.templatename = "!";
this.template = re;
}
/**
* Метод позволяет установить обработчики для команд (частей составляющих команды)
*
* @param {String} - задает имя, под которым будет находится текущая часть
* команды в context.params. По данному имени в обработчике
* можно будет обратиться к соответствующей части команды
* @param {String|Number|Function|RegExp} - задает шаблон соответствия для части
* команды. Шаблон может быть строкой, числом, функцией и
* регулярным выражением. В случае если шаблон это функция, то
* она должна возвращать true если текущая часть команды
* соответствует условиям и false если нет
* @param {...[Function|Router]} - массив обработчиков, может быть функцией или
* объектом класса Router. При вызове функции обработчика ей
* передаются 2 параметра: context{Object} и next{Function}.
* В контексте содержатся свойства:
* params - объект с именованными значениями частей введенной команды
* например если была введена команда:
* /help join
* в params будет:
* {
* "prefix": "/",
* "заданное имя команды": "help",
* "заданное имя части": "join"
* }
* list - массив, состоящий из частей команды.
* например если была введена команда:
* /help join
* в params будет:
* ["/","help","join"]
* current - значение части команды обрабатываемое в данный момент
*/
on(templatename, template, ...listeners){
listeners = validateArguments(templatename, template, ...listeners);
if (!listeners)
return;
listeners = listeners.map(item=>{
if (typeof item === "function")
return item;
item.init(this, templatename, template);
return function(...args){ item.route(...args); };
})
listeners.forEach(handler=>{
this.listeners.push({
templatename: templatename,
template: template,
handler: handler
});
})
}
/**
* Метод позволяет установить обработчики для команд (частей составляющих команды)
* не проверяющие на соответствие шаблону текущей части команды. Такие обработчики
* будут выполнены всегда, если выполнение дойдет до роутера в котором они объявлены.
*
* @param {...[Function|Router]} - массив обработчиков. аналогичен описанному в методе Route.on
*/
use(...listeners){
this.on(undefined, re, ...listeners);
}
/**
* Для внутреннего использования
*/
init(parent, templatename, template){
this.parent = parent;
this.templatename = templatename;
this.template = template;
};
/**
* Метод инициирует процесс обработки пришедшей команды. В результате
* процесса будут вызваны соответствующие команде и ее частям обработчики
* @param {String} - введенная команда
*/
parse(command){
if (!command || typeof command !== "string") return;
const list = [];
const prefix = command[0];
if( this.options.prefix ){
if (Array.isArray(this.options.prefix) && !this.options.prefix.includes(prefix)) return;
if (typeof this.options.prefix === "string" && this.options.prefix !== prefix) return;
list.push(prefix);
}
list.push(...command.substr(1).split(/\s+/));
if( !list || !list.length ) return;
this.route({
params: { prefix: prefix },
list: list,
index: 0,
current: list[0],
queues: []
},()=>{});
}
/**
* В случае если в качестве обработчика был передан объект класса Route
* процессе обработки поступающих команд в качестве обработчика будет
* вызываться этот метод переданного объекта
* @param {Object} - объект с контекстом обработки команды
* @param {Function} - функция next() для передачи эстафеты следующему
* обработчику. В случае если next не будет вызвана
* процесс обработки текущей команды будет прерван.
*/
route(ctx, next){
const context = {...ctx};
context.index++;
const current = context.current = context.list[context.index];
let index = 0;
let item = this.listeners[index];
const _start = ()=>{
setImmediate(()=>{
// console.log("listener:", item);
if (item.template instanceof RegExp) {
if (!item.template.test(current))
return _next();
} if (typeof item.template === "function") {
if (!item.template(current))
return _next();
} if (typeof item.template === "string" || typeof item.template === "number") {
if (current != item.template)
return _next();
}
if(item.templatename)
context.params[item.templatename] = context.current;
item.handler(context,()=>{
_next();
});
});
}
const _next = ()=>{
index++;
item = this.listeners[index];
if(index >= this.listeners.length )
return next();
_start();
}
_start();
}
}
module.exports = (options)=>{
return new Router(options);
};
/**
* Далее временные костыли, их можно не смотреть
*/
function validateTemplateName(templatename){
if (templatename === undefined)
return true;
if (templatename && typeof templatename === "string")
return true;
return false;
}
function validateTemplate(template){
if (!template &&
typeof template !== "string" &&
typeof template !== "function" &&
!Array.isArray(template) &&
!(template instanceof RegExp))
return false;
return true;
}
function validateListeners(...listeners){
if (!listeners)
return false;
listeners = listeners.filter(h=>{
if (typeof h === "function")
return true;
if (h instanceof Router)
return true;
return false;
});
if(!listeners.length)
return false;
return listeners;
}
function validateArguments(templatename, template, ...listeners){
if (!validateTemplateName(templatename))
return;
if (!validateTemplate(template))
return;
return validateListeners(...listeners);
}