/*jshint node:true*/ "use strict"; var utils = require("./utils"); var regexpSlashes = /\//g, regexpTrimSlashes = /^\/|\/$/g, regexpTrailingStar = /\*$/, regexpAllNamedParameters = /:[^\/]+/g; /*! * Generic helpers */ var compiledCache = {}; function compilePattern(pattern, matchSubPaths) { var cacheKey = matchSubPaths ? pattern + "[/*]" : pattern; if (!(cacheKey in compiledCache)) { var compiled = { raw: pattern, key: cacheKey }; var regexp = "^\\/" + pattern .replace(regexpSlashes, "\\/") .replace(regexpAllNamedParameters, "([^\\/]+)") .replace(regexpTrailingStar, "(.*)$"); compiled.trailingStar = !!(pattern.match(regexpTrailingStar)); if (!compiled.trailingStar && matchSubPaths) { compiled.regexp = new RegExp(regexp + "(\\/.*)?$"); } else { compiled.regexp = new RegExp(compiled.trailingStar ? regexp : (regexp + "$")); } compiled.names = (pattern.match(regexpAllNamedParameters) || []).map(function(name) { return name.substr(1); }); if (compiled.trailingStar) { compiled.names.push("*"); } compiledCache[cacheKey] = compiled; } return compiledCache[cacheKey]; } function addHandler(handlers, compiled, method, handler) { var item = Object.create(compiled); item.method = method; item.handler = handler; handlers.push(item); return item; } function addHook(handlers, compiled, hook, strict) { if (!strict && !compiled.trailingStar) { compiled = compilePattern(compiled.raw, true); } var item = Object.create(compiled); item.hook = hook; handlers.push(item); return item; } function addOptions(handlers, compiled, options) { var item = Object.create(compiled); item.options = options; handlers.push(item); return item; } /*! * Path matcher */ function Path(root, pattern) { this.root = root; this.compiled = compilePattern(pattern); if (this.compiled.trailingStar) { this.sub = undefined; this.remove = undefined; } } "get list count post put del".split(" ").forEach(function(method) { Path.prototype[method] = function(handler) { addHandler(this.root.handlers, this.compiled, method, handler); return this; }; }); Path.prototype.hook = function(hook, strict) { addHook(this.root.handlers, this.compiled, hook, strict); return this; }; Path.prototype.readonly = function(subsToo) { var path = this; "post put del".split(" ").forEach(function(method) { path[method](undefined); if (subsToo) { path.sub("*")[method](undefined); } }); return this; }; Path.prototype.sub = function(pattern, hook) { pattern = pattern.replace(regexpTrimSlashes, ""); return this.root.sub(this.compiled.raw + "/" + pattern, hook); }; Path.prototype.remove = function(pattern) { pattern = pattern.replace(regexpTrimSlashes, ""); return this.root.remove(this.compiled.raw + "/" + pattern); }; Path.prototype.set = function(key, value, strict) { if (typeof key === "object") { strict = value; } var compiled = this.compiled; if (!strict && !compiled.trailingStar) { compiled = compilePattern(compiled.raw, true); } var hook = this.root.handlers.filter(function(h) { return h.options && h.raw === compiled.raw; })[0]; if (!hook) { hook = addOptions(this.root.handlers, compiled, {}); } if (typeof key === "object") { Object.keys(key).forEach(function(k) { hook.options[k] = key[k]; }); } else { hook.options[key] = value; } return this; }; /*! * Base hooks */ function getParamHook(names, match) { // Strip full match var values = match.slice(1); return function paramHook(req, next) { req.params = req.params || {}; names.forEach(function(name) { var value = values.shift(); if (name === "*") { req.params[name] = value; } else { req.params[name] = decodeURIComponent(value); } }); next(); }; } function getOptionsHook() { var hook = function optionsHook(req, next) { req.options = req.options || {}; Object.keys(hook.options).forEach(function(key) { req.options[key] = hook.options[key]; }); next(); }; hook.options = {}; return hook; } function getHref(subpath) { /*jshint validthis:true */ var req = this; var path = req.path.replace(regexpTrimSlashes, ""); if (subpath) { path = path + "/" + subpath.replace(regexpTrimSlashes, ""); } return utils.getHref(req, path); } function matchPattern(pattern, path) { /*jshint validthis:true */ var compiled = compilePattern(pattern); var req = this; var match = (path || req.path).match(compiled.regexp); if (match) { var values = match.slice(1); var params = {}; compiled.names.forEach(function(name) { params[name] = values.shift(); }); return params; } return false; } var defaultHooks = [ /* Add request helpers */ function requestHelpersHook(req, next) { req.getHref = getHref; req.match = matchPattern; next(); } ]; /*! * Root resource */ function rootResource() { var root = { handlers: [], sub: function(pattern, hook) { var path = new Path(root, pattern.replace(regexpTrimSlashes, "")); if (hook) { path.hook(hook); } return path; }, remove: function(pattern) { pattern = pattern.replace(regexpTrimSlashes, ""); function filterFunc(h) { var raw = h.raw; if (raw.substr(0, pattern.length) === pattern) { if (raw.length === pattern.length || raw[pattern.length] === "/") { return false; } } return true; } root.handlers = root.handlers.filter(filterFunc); }, match: function(req) { var matchingHooks = defaultHooks.slice(0); var spec = {}; var matchedPatterns = []; /* Add options hook */ var optionsHook = getOptionsHook(); matchingHooks.push(optionsHook); /* Find handlers matching requested path */ root.handlers.forEach(function(h) { var match = req.path.match(h.regexp); if (match) { /* Add parameter hook only once for each pattern */ if (h.names.length > 0 && matchedPatterns.indexOf(h.raw) === -1) { matchingHooks.push(getParamHook(h.names, match)); matchedPatterns.push(h.raw); } if (h.options) { var options = optionsHook.options; Object.keys(h.options).forEach(function(key) { options[key] = h.options[key]; }); } if (h.hook) { matchingHooks.push(h.hook); } if (h.method) { // get and count/list override each other if (h.method === "get") { delete spec.count; delete spec.list; } if (h.method === "count" || h.method === "list") { delete spec.get; } spec[h.method] = h.handler; } } }); if (Object.keys(spec).length) { return { spec: spec, hooks: matchingHooks }; } } }; return root; } module.exports = rootResource;