UNPKG

uri-template-router

Version:

Match a URI to a pattern in a collection of URI Templates

399 lines (360 loc) 13 kB
'use strict'; module.exports.range = range; function *range(str){ for(var i=0; i<str.length; i++){ const chr = str[i]; if(chr !== '-' && str[i+1]==='-' && str[i+2]){ for(var j=str.charCodeAt(i), end=str.charCodeAt(i+2); j<=end; j++){ yield String.fromCharCode(j); } i += 2; }else{ yield chr; } } } const rangeSets = new Map; // A node on the tree is a list of various options to try to match against an input character. // The "next" and "list_set" options specify another branch to also try and match against the current input character. // The "template_match" option specifies the end of the template was reached, and to return a successful match result. This is usually only reachable immediately after matching an EOF. module.exports.Node = Node; function Node(transitions, partials, final){ if(typeof partials === 'number') throw new Error; partials = partials || {}; this.transitions = transitions || {}; // if(Object.keys(partials).length===0){ // throw new Error('Expected partial match info'); // } // Maps final state -> information about the meaning of this transition given the final state this.partials = partials || {}; // If we reach this branch, declare a match for this template this.final = final || false; this.classes = Object.keys(transitions).filter(v => v.length>1).sort((a,b) => a.length-b.length); } Node.prototype.get = function get(chr){ if(this.transitions[chr] !== undefined){ return this.transitions[chr]; } // If the chr is a pct-encoded sequence and there's no individual transition, // then it matches a character class transition if defined if(chr[0]==='%' && chr.length===3){ if(this.transitions["-.0-9A-Z_a-z~"] !== undefined){ return this.transitions["-.0-9A-Z_a-z~"]; }else if(this.transitions["-.0-9A-Z_a-z~:/?#[]@!$&'()*+,;="] !== undefined){ return this.transitions["-.0-9A-Z_a-z~:/?#[]@!$&'()*+,;="]; } } for(const tr of this.classes){ if(!rangeSets.has(tr)){ rangeSets.set(tr, new Set(range(tr))); } if(rangeSets.get(tr).has(chr)){ return this.transitions[tr]; } } // FIXME this is a huge hack that does not scale to multiple character classes if(chr === "-.0-9A-Z_a-z~" && this.transitions["-.0-9A-Z_a-z~:/?#[]@!$&'()*+,;="]!==undefined){ return this.transitions["-.0-9A-Z_a-z~:/?#[]@!$&'()*+,;="]; } }; module.exports.verify = verify; function verify(fsm){ if(!Array.isArray(fsm)){ throw new Error('Expected `fsm` to be an Array'); } fsm.forEach(function(st, i){ if(typeof st !== 'object'){ throw new Error('Expected fsm[i] to be an object'); } for(var symbol in st.transitions){ if(typeof st.transitions[symbol] !== 'number'){ throw new Error('Expected fsm['+JSON.stringify(i)+']['+JSON.stringify(symbol)+'] to be a number, got '+typeof st[symbol]); } if(st.transitions[symbol] >= fsm.length){ throw new Error('Expected fsm['+JSON.stringify(i)+']['+JSON.stringify(symbol)+'] to be a state in `fsm`'); } } }); } module.exports.union = union; function union(fsms, ordering){ function partial_union(states){ const map = {}; for(var i=0; i<states.length; i++){ if(states[i]===undefined) continue; for(var k in states[i].partials){ map[k] = states[i].partials[k]; } } return map; } function final_union(states){ function sort_final(a, b){ return ordering.get(a.route) - ordering.get(b.route); } if(states.some(final => final && (Array.isArray(final) ? final.length : final))){ const items = states.flatMap(final => (final && Array.isArray(final)) ? final : []); if(ordering) items.sort(sort_final); return items.length ? items : true; }else{ return false; } } return parallel(fsms, partial_union, final_union); } module.exports.parallel = parallel; function parallel(fsms, partial, final){ if(!Array.isArray(fsms)) throw new Error('Expected `fsms` to be an array of arrays'); fsms.forEach(verify); // By convention, start on the 0 state const cross_product_list = [ fsms.map(v=>0) ]; // A handy mapping of each cross-product state to its new state const cross_product_map = new Map([[fsms.map(v=>0).join(','), 0]]); // The new states const combination_states = []; // iterate over a growing list for(var i=0; i<cross_product_list.length; i++){ const state_i = cross_product_list[i]; const state = state_i.map( (i,j) => fsms[j][i] ); // Compute the symbols used by each state // const alphabet = new Set(fsms.flatMap(v => [...v.transitions.keys()])); const alphabet = new Set(state.flatMap(fsm_st => fsm_st ? Object.keys(fsm_st.transitions) : [])); // compute map for this state const transitions = {}; for(const symbol of alphabet){ const next = state.map(function(fsm_st){ // Returning undefined is OK return fsm_st && fsm_st.get(symbol); }); // Generate a key name for this cross-product const nextKey = next.join(','); const nextId = cross_product_map.get(nextKey); if(nextId !== undefined){ // If there is already a state representing this cross-product, point to that transitions[symbol] = nextId; }else{ // Create a new state transitions[symbol] = cross_product_list.length; cross_product_map.set(nextKey, cross_product_list.length); cross_product_list.push(next); } } combination_states[i] = new Node(transitions, partial(state), final(state.map(v => v && v.final))); } return combination_states; } module.exports.concat = concat; function concat(fsms){ if(!Array.isArray(fsms)) throw new Error('Expected `fsms` to be an array of arrays'); fsms.forEach(verify); function connect_all(fsm_i, substate){ /* Take a state in the numbered FSM and return a set containing it, plus (if it's final) the first state from the next FSM, plus (if that's final) the first state from the next but one FSM, plus... */ const result = [ [fsm_i, substate] ]; for(var i=fsm_i; i<fsms.length-1 && fsms[i][substate].final; i++){ result.push([i+1, 0]); substate = 0; } // TODO Ignore states that have no outgoing transitions return result.sort(); } // Maps new state ids to one of the items in the powerset // Start with the first fsm's (0) initial state (0) const powerset_list = [ connect_all(0, 0) ]; // A handy mapping of each cross-product state to its new state const powetset_id_map = new Map([['0,0', 0]]); // The new fsm after concatenation const concat_states = []; // iterate over a growing list for(var i=0; i<powerset_list.length; i++){ const powetset_item = powerset_list[i]; const state = powetset_item.map( ([j,i])=>[j, fsms[j][i]] ); // Compute the symbols used by each state const alphabet = new Set(state.flatMap(fsm_st => Object.keys(fsm_st[1].transitions))); // compute map for this state const transitions = {}; for(const symbol of alphabet){ const next_all = state.flatMap(function(fsm_st){ const [fsm_i, fsm_node] = fsm_st; // returning undefined is OK if(fsm_node.get(symbol) !== undefined){ return connect_all(fsm_i, fsm_node.get(symbol)); } return []; }).sort(); // Remove duplicates var previous='', next = next_all.filter(function(v){ return (previous.toString()!==(previous=v).toString()); }); if(!next.length) continue; // Generate a key name for this cross-product const nextKey = next.join(','); const nextId = powetset_id_map.get(nextKey); if(nextId !== undefined){ // Use an existing state representing this cross-product, if available transitions[symbol] = nextId; }else{ // Create a state for a new cross-product combination transitions[symbol] = powerset_list.length; powetset_id_map.set(nextKey, powerset_list.length); powerset_list.push(next); } } const partial_matches = Object.fromEntries(state.flatMap(function(v){ const [_, part] = v; return part ? Object.entries(part.partials) : []; })); const final = powetset_item.some(function(state_val){ const [fsm_i, substate] = state_val; if(fsms[fsm_i][substate].final.length===0) throw new Error('Zero-length final'); return fsm_i==fsms.length-1 && fsms[fsm_i][substate].final; }); concat_states[i] = new Node(transitions, partial_matches, final); } return concat_states; } module.exports.fromString = fromString; function fromString(expression, partial){ const fsm = []; for(var i=0; i<expression.length; i++){ var chr = expression[i]; if(chr==='%'){ chr += expression[i+1] + expression[i+2]; i += 2; } fsm.push(new Node({[chr]:fsm.length+1}, partial && partial(i))); } fsm.push(new Node({}, {}, true)); return fsm; } module.exports.optional = optional; function optional(f){ const epsilon = [ new Node({}, {}, true) ]; return union([f, epsilon]); } module.exports.star = star; function star(f){ const alphabet = f.flatMap(state => Object.keys(state.transitions)); const initial = [0]; function follow(current, symbol){ const next = new Set(); current.forEach(function(substate){ if(f[substate] && f[substate].get(symbol)){ next.add(f[substate].get(symbol)); } // If one of our substates is final, then we can also consider // transitions from the initial state of the original FSM. if(f[substate].final && f[0].get(symbol)){ next.add(f[0].get(symbol)); } }); if(next.size === 0) return; return [...next].sort(); } function final(state){ return state.some( (substate)=> f[substate].final ); } const states_list = [initial]; const states_map = new Map([[initial.toString(), 0]]); const states = []; // iterate over a growing list for(var i=0; i<states_list.length; i++){ const state = states_list[i]; // compute map for this state const transitions = {}; for(const symbol of alphabet){ const next = follow(state, symbol); if(next===undefined) continue; const nextKey = next.join(','); const nextId = states_map.get(nextKey); if(nextId !== undefined){ // If there is already a state representing this cross-product, point to that transitions[symbol] = nextId; }else{ // Create a new state transitions[symbol] = states_list.length; states_map.set(nextKey, states_list.length); states_list.push(next); } } states[i] = new Node(transitions, {}, final(state)); } return optional(states); } // This probably isn't needed module.exports.reverse = reverse; function reverse(f){ const alphabet = f.flatMap(state => Object.keys(state.transitions)); const initial = f.map((v, i) => v.final ? i : undefined).filter(v => v!==undefined); const powerset_list = [initial]; const powerset_stateid_map = new Map([[initial.join(','), 0]]); const states = []; // states[0] will be filled in by the first iteration of this loop // iterate over a growing list for(var i=0; i<powerset_list.length; i++){ const powerset_combination = powerset_list[i]; // compute map for this state const transitions = {}; for(const symbol of alphabet){ const next = f.map(function(state, i){ return powerset_combination.some(function(state0){ return state.get(symbol)===state0; }) ? i : undefined; }).filter(v => (v!==undefined)).sort(); if(next.length===0) continue; const nextKey = next.join(','); const nextIdx = powerset_stateid_map.get(nextKey); if(nextIdx !== undefined){ // If there is already a state with identical transitions, point to that transitions[symbol] = nextIdx; }else{ // Create a new state transitions[symbol] = powerset_list.length; powerset_stateid_map.set(nextKey, powerset_list.length); powerset_list.push(next); } } // Trim character transitions that are the same as the group transition for(const tr in transitions){ if(tr.length <= 1 || tr[0]==='%') continue; var shadowed = true; // TODO trim pct-encoded sequences that are the same as the character class // And trim character classes that are the same as larger character classes for(const chr of range(tr)){ if(transitions[chr]===transitions[tr]){ delete transitions[chr]; } if(!transitions[chr]){ shadowed = false; } } if(shadowed){ delete transitions[tr]; } } states[i] = new Node(transitions, {}, powerset_combination.indexOf(0) >= 0); } return states; } module.exports.reduce = reduce; function reduce(f){ return reverse(reverse(f)); } module.exports.compare = compare; function compare(fsms){ if(fsms.length !== 2){ throw new Error('Expected 2 fsms to compare'); } var isSuperset = true, isSubset = true, isDisjoint = true; function partial_union(states){ } function final_union(states){ const a = Array.isArray(states[0]) ? states[0].length : states[0]; const b = Array.isArray(states[1]) ? states[1].length : states[1]; if(!a && b) isSuperset = false; if(a && !b) isSubset = false; if(!!a && !!b) isDisjoint = false; // set to false when there are some final elements in common } parallel(fsms, partial_union, final_union); return [isSuperset, isSubset, isDisjoint]; }