'use strict'
const url = require('url')
const _ = require('lodash')
const pathToRegexp = require('path-to-regexp')
const InvalidUriParameterError = require('../errors/invalid-uri-parameter-error')
const InvalidUriError = require('../errors/invalid-uri-error')
/**
* Parses URI with awareness of local resources.
*
* Knows about the different resource endpoints and can parse URIs returning
* the type of resource and any useful parameters. It can also construct URIs
* based on type and parameters.
*/
class UriManager {
/**
* Constructor.
*
* @param {String} base Should correspond to the public base URI.
*/
constructor (base) {
this.base = base
this.baseParsed = url.parse(base)
this.routes = []
this.types = {}
}
/**
* Register a new resource type.
*
* This is called initially during startup to define the different resources
* and their corresponding URI endpoints.
*
* @param {String} type Type of resource.
* @param {String} path Path (route) for this resource, e.g. '/foos/:id'
*/
addResource (type, path) {
const keys = []
const re = pathToRegexp(path, keys)
const keyNames = _.map(keys, 'name')
// The first result of the regex will be the whole path
keyNames.unshift('path')
this.routes.push(function (localPart) {
// Check if this route matches the URI
const match = re.exec(localPart)
if (!match) {
return false
}
// Parse the URI extracting any parameters
const params = _.zipObject(keyNames, match)
// We also want to indicate which URI matched
params.type = type
return params
})
this.types[type] = {
compiler: pathToRegexp.compile(path),
keys: keys
}
}
/**
* Determines if a URI matches any local resource.
*
* This method will compare a URI first against the local base URI to
* determine if it is a local URI at all. Then, it will compare it against
* all registered local resource URI types. If any match it will parse the
* URI and return an object with information about the type of resource and
* any parameters that were included in the URI.
*
* @param {String} uri URI to be parsed/analyzed
* @param {String} requiredType If provided, URI must be of this type or an error is raised.
* @return {Object} Description of the URI with contextual information.
*/
parse (uri, requiredType) {
const parsed = url.parse(uri)
// Match to the base path such that we enforce the right case-sensitivity:
// - protocol is not case sensitive
// - host is not case sensitive
// - path is case sensitive
parsed.local =
_.startsWith(uri.toLowerCase(), this.base.toLowerCase()) &&
_.startsWith(parsed.path, this.baseParsed.path)
// For local URIs we want to know if it matches a specific local resource
if (parsed.local && parsed.hash === null && parsed.search === null) {
const localPart = uri.slice(this.base.length)
// Test against all defined local routes
for (let route of this.routes) {
const match = route(localPart)
if (match) {
// If a route matches, copy the properties into our result
_.merge(parsed, match)
// And don't try any more routes
break
}
}
}
if (requiredType &&
(!parsed.local || parsed.type !== requiredType)) {
throw new InvalidUriError('URI is not a valid ' + requiredType + ' URI: ' + uri)
}
return parsed
}
/**
* Create a new URI based on a type and ordered parameters.
*
* @param {String} type Type of URI
* @param {...*} params List of values to fill in to URI parameters
* @return {String} Complete, absolute URI
*/
make (type) {
const typeInfo = this.types[type]
const paramsList = Array.prototype.slice.call(arguments, 1)
if (!typeInfo) {
throw new InvalidUriError('Unknown resource type provided')
}
// Convert params list to object
if (typeInfo.keys.length !== paramsList.length) {
throw new InvalidUriParameterError('Incorrect parameter count provided')
}
const params = {}
typeInfo.keys.forEach(function (key, i) {
params[key.name] = paramsList[i]
})
return this.makeWithParams(type, params)
}
/**
* Create a new URI based on a type and ordered parameters.
*
* @param {String} type Type of URI
* @param {Object} params Parameters to fill in
* @return {String} Complete, absolute URI
*/
makeWithParams (type, params) {
const typeInfo = this.types[type]
if (!typeInfo) {
throw new InvalidUriError('Unknown resource type provided')
}
return this.base + typeInfo.compiler(params)
}
}
exports.UriManager = UriManager