path-trie.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import {Errorf} from '@e22m4u/js-format';
  2. import {pathToRegexp} from 'path-to-regexp';
  3. import {createDebugger} from './utils/index.js';
  4. /**
  5. * @typedef {{
  6. * token: string,
  7. * regexp: RegExp | undefined,
  8. * names: string[],
  9. * value: *,
  10. * children: {[token: string]: Node},
  11. * }} Node
  12. *
  13. * @typedef {{value: *, params: object}} ResolvedValue
  14. * @typedef {{node: Node, params: object}} ResolvedNode
  15. */
  16. /**
  17. * Debug.
  18. *
  19. * @type {Function}
  20. */
  21. const debug = createDebugger();
  22. /**
  23. * Path trie.
  24. */
  25. export class PathTrie {
  26. /**
  27. * Root node.
  28. *
  29. * @type {Node}
  30. * @private
  31. */
  32. _root = {
  33. token: '',
  34. regexp: undefined,
  35. names: [],
  36. value: undefined,
  37. children: {},
  38. };
  39. /**
  40. * Add value.
  41. *
  42. * @param {string} pathTemplate
  43. * @param {*} value
  44. * @returns {this}
  45. */
  46. add(pathTemplate, value) {
  47. if (typeof pathTemplate !== 'string')
  48. throw new Errorf(
  49. 'The first argument of PathTrie.add should be ' +
  50. 'a String, but %v given.',
  51. pathTemplate,
  52. );
  53. if (value == null)
  54. throw new Errorf(
  55. 'The second argument of PathTrie.add is required, but %v given.',
  56. value,
  57. );
  58. debug('Adding the value to %v.', pathTemplate);
  59. const tokens = pathTemplate.split('/').filter(Boolean);
  60. this._createNode(tokens, 0, value, this._root);
  61. return this;
  62. }
  63. /**
  64. * Match value.
  65. *
  66. * @param {string} path
  67. * @returns {ResolvedValue|undefined}
  68. */
  69. match(path) {
  70. if (typeof path !== 'string')
  71. throw new Errorf(
  72. 'The first argument of PathTrie.match should be ' +
  73. 'a String, but %v given.',
  74. path,
  75. );
  76. debug('Matching a value with the path %v.', path);
  77. const tokens = path.split('/').filter(Boolean);
  78. const params = {};
  79. const result = this._matchNode(tokens, 0, params, this._root);
  80. if (!result || !result.node.value) return;
  81. return {value: result.node.value, params};
  82. }
  83. /**
  84. * Create node.
  85. *
  86. * @param {string[]} tokens
  87. * @param {number} index
  88. * @param {*} value
  89. * @param {Node} parent
  90. * @returns {Node}
  91. * @private
  92. */
  93. _createNode(tokens, index, value, parent) {
  94. // если массив токенов пуст, а индекс нулевой,
  95. // то проверяем возможность установки значения
  96. // в родителя
  97. if (tokens.length === 0 && index === 0) {
  98. // если корневой узел не имеет
  99. // значения, то устанавливаем
  100. if (parent.value == null) {
  101. parent.value = value;
  102. }
  103. // если корневой узел имеет значение
  104. // отличное от устанавливаемого,
  105. // то выбрасываем ошибку
  106. else if (parent.value !== value) {
  107. throw new Errorf('The duplicate path "" has a different value.');
  108. }
  109. debug('The value has set to the root node.');
  110. return parent;
  111. }
  112. // проверка существования токена
  113. // по данному индексу
  114. const token = tokens[index];
  115. if (token == null)
  116. throw new Errorf(
  117. 'Invalid index %v has passed to the PathTrie._createNode.',
  118. index,
  119. );
  120. // проверка существования узла
  121. // по текущему токену
  122. let child = parent.children[token];
  123. const isLast = tokens.length - 1 === index;
  124. if (child) {
  125. // если узел не является последним,
  126. // то переходим к следующему
  127. if (!isLast) {
  128. debug('The node %v already exist.', token);
  129. return this._createNode(tokens, index + 1, value, child);
  130. }
  131. // если узел является последним,
  132. // то проверяем наличие значения
  133. else {
  134. debug('The node %v already exist.', token);
  135. // если существующий узел не имеет
  136. // значения, то устанавливаем текущее
  137. if (child.value == null) {
  138. debug('The node %v has the same value.', token);
  139. child.value = value;
  140. }
  141. // если существующий узел имеет
  142. // значение отличное от текущего,
  143. // то выбрасываем ошибку
  144. else if (child.value !== value) {
  145. throw new Errorf(
  146. 'The duplicate path %v has a different value.',
  147. '/' + tokens.join('/'),
  148. );
  149. }
  150. // так как данный токен является последним,
  151. // то возвращаем существующий узел
  152. return child;
  153. }
  154. }
  155. debug('The node %v does not exist.', token);
  156. // создаем новый узел, и если токен является
  157. // последним, то сразу устанавливаем значение
  158. child = {
  159. token,
  160. regexp: undefined,
  161. names: [],
  162. value: undefined,
  163. children: {},
  164. };
  165. if (isLast) {
  166. debug('The node %v is last.', token);
  167. child.value = value;
  168. }
  169. // если токен содержит параметры,
  170. // то записываем их имена и регулярное
  171. // выражение в создаваемый узел
  172. if (token.indexOf(':') > -1) {
  173. debug('The node %v has parameters.', token);
  174. // если токен содержит неподдерживаемые
  175. // модификаторы, то выбрасываем ошибку
  176. const modifiers = /([?*+{}])/.exec(token);
  177. if (modifiers)
  178. throw new Errorf(
  179. 'The symbol %v is not supported in path %v.',
  180. modifiers[0],
  181. '/' + tokens.join('/'),
  182. );
  183. // определение регулярного выражения
  184. // и параметров текущего токена
  185. let regexp, keys;
  186. try {
  187. const regexpAndKeys = pathToRegexp(token);
  188. regexp = regexpAndKeys.regexp;
  189. keys = regexpAndKeys.keys;
  190. } catch (error) {
  191. // если параметры не найдены, то выбрасываем
  192. // ошибку неправильного использования
  193. // символа ":"
  194. if (error.message.indexOf('Missing parameter') > -1)
  195. throw new Errorf(
  196. 'The symbol ":" should be used to define path parameters, ' +
  197. 'but no parameters found in the path %v.',
  198. '/' + tokens.join('/'),
  199. );
  200. // если ошибка неизвестна,
  201. // то выбрасываем как есть
  202. throw error;
  203. }
  204. // записываем имена параметров и регулярное
  205. // выражение в создаваемый узел
  206. if (Array.isArray(keys) && keys.length) {
  207. child.names = keys.map(p => `${p.name}`);
  208. child.regexp = regexp;
  209. }
  210. // если параметры не найдены, то выбрасываем
  211. // ошибку неправильного использования
  212. // символа ":"
  213. else {
  214. throw new Errorf(
  215. 'The symbol ":" should be used to define path parameters, ' +
  216. 'but no parameters found in the path %v.',
  217. '/' + tokens.join('/'),
  218. );
  219. }
  220. debug('Found parameters are %l.', child.names);
  221. }
  222. // записываем новый узел в родителя
  223. parent.children[token] = child;
  224. debug('The node %v has created.', token);
  225. // если текущий узел является последним,
  226. // то возвращаем его, или продолжаем
  227. // смещать индекс
  228. if (isLast) return child;
  229. return this._createNode(tokens, index + 1, value, child);
  230. }
  231. /**
  232. * Match node.
  233. *
  234. * @param {string[]} tokens
  235. * @param {number} index
  236. * @param {object} params
  237. * @param {Node} parent
  238. * @returns {ResolvedNode|undefined}
  239. * @private
  240. */
  241. _matchNode(tokens, index, params, parent) {
  242. // если массив токенов пуст, а индекс нулевой,
  243. // то проверяем наличие значения в родителе
  244. if (tokens.length === 0 && index === 0) {
  245. if (parent.value) {
  246. debug(
  247. 'The path %v matched with the root node.',
  248. '/' + tokens.join('/'),
  249. );
  250. return {node: parent, params};
  251. }
  252. // если родительский узел не имеет
  253. // значения, то возвращаем "undefined"
  254. return;
  255. }
  256. // проверка существования токена
  257. // по данному индексу
  258. const token = tokens[index];
  259. if (token == null)
  260. throw new Errorf(
  261. 'Invalid index %v has passed to the PathTrie._matchNode.',
  262. index,
  263. );
  264. // если текущий токен не соответствует
  265. // ни одному узлу, то возвращаем "undefined"
  266. const resolvedNodes = this._matchChildrenNodes(token, parent);
  267. debug('%v nodes matches the token %v.', resolvedNodes.length, token);
  268. if (!resolvedNodes.length) return;
  269. // если текущий токен последний,
  270. // то возвращаем первый дочерний
  271. // узел, который имеет значение
  272. const isLast = tokens.length - 1 === index;
  273. if (isLast) {
  274. debug('The token %v is last.', token);
  275. for (const child of resolvedNodes) {
  276. debug('The node %v matches the token %v.', child.node.token, token);
  277. if (child.node.value) {
  278. debug('The node %v has a value.', child.node.token);
  279. const paramNames = Object.keys(child.params);
  280. if (paramNames.length) {
  281. paramNames.forEach(name => {
  282. debug(
  283. 'The node %v has parameter %v with the value %v.',
  284. child.node.token,
  285. name,
  286. child.params[name],
  287. );
  288. });
  289. } else {
  290. debug('The node %v has no parameters.', child.node.token);
  291. }
  292. Object.assign(params, child.params);
  293. return {node: child.node, params};
  294. }
  295. }
  296. }
  297. // если токен промежуточный, то проходим
  298. // вглубь каждого дочернего узла
  299. else {
  300. for (const child of resolvedNodes) {
  301. const result = this._matchNode(tokens, index + 1, params, child.node);
  302. if (result) {
  303. debug('A value has found for the path %v.', '/' + tokens.join('/'));
  304. const paramNames = Object.keys(child.params);
  305. if (paramNames.length) {
  306. paramNames.forEach(name => {
  307. debug(
  308. 'The node %v has parameter %v with the value %v.',
  309. child.node.token,
  310. name,
  311. child.params[name],
  312. );
  313. });
  314. } else {
  315. debug('The node %v has no parameters.', child.node.token);
  316. }
  317. Object.assign(params, child.params);
  318. return result;
  319. }
  320. }
  321. }
  322. // если поиск по дочерним узлам
  323. // родителя не привел к результату,
  324. // то возвращаем "undefined"
  325. debug('No matched nodes with the path %v.', '/' + tokens.join('/'));
  326. return undefined;
  327. }
  328. /**
  329. * Match children nodes.
  330. *
  331. * @param {string} token
  332. * @param {Node} parent
  333. * @returns {ResolvedNode[]}
  334. * @private
  335. */
  336. _matchChildrenNodes(token, parent) {
  337. const resolvedNodes = [];
  338. // если найден узел по литералу токена,
  339. // то нет необходимости продолжать поиск
  340. // по узлам с параметрами, а можно немедленно
  341. // вернуть его в качестве результата
  342. let child = parent.children[token];
  343. if (child) {
  344. resolvedNodes.push({node: child, params: {}});
  345. return resolvedNodes;
  346. }
  347. // поиск по узлам с параметрами выполняется
  348. // путем сопоставления токена с регулярным
  349. // выражением каждого узла
  350. for (const key in parent.children) {
  351. child = parent.children[key];
  352. if (!child.names || !child.regexp) continue;
  353. const match = child.regexp.exec(token);
  354. if (match) {
  355. const resolved = {node: child, params: {}};
  356. // так как параметры имеют тот же порядок,
  357. // что и вхождения, последовательно перебираем,
  358. // и присваиваем вхождения с соответствующим
  359. // индексом в качестве значений
  360. let i = 0;
  361. for (const name of child.names) {
  362. const val = match[++i];
  363. resolved.params[name] = decodeURIComponent(val);
  364. }
  365. // добавление узла к результату
  366. resolvedNodes.push(resolved);
  367. }
  368. }
  369. return resolvedNodes;
  370. }
  371. }