Source: lib/url.js

// on pourrait se débrouiller avec https://nodejs.org/dist/latest-v8.x/docs/api/url.html
// mais c'est plus simple avec
// @see https://nodejs.org/dist/latest-v8.x/docs/api/querystring.html
// (et on préfère son comportement sur les params multiples)
const querystring = require('querystring')
const { getNormalizedGrabOptions } = require('./normalize')
const { hasProp } = require('sesajstools')

const { baseUrl } = require('./config')

// fonctions privées

/**
 * throw ou console.error suivant isStrict et retourne value
 * @private
 * @param {string} errorMessage
 * @param {boolean|object} isStrict
 * @param {boolean} [isStrict.strict]
 * @param {string} [value='']
 * @return {string} value
 * @throws {Error} si isStrict
 */
function errorHandler (errorMessage, isStrict, value = '') {
  if (typeof isStrict === 'object') {
    isStrict = hasProp(isStrict, 'strict') ? !!isStrict.strict : true
  }
  if (isStrict) throw new Error(errorMessage)
  console.error(new Error(errorMessage))
  return value
}

// fonctions exportées

/**
 * Retourne un paramètre d'une url
 * @param {string} url
 * @param {string} name
 * @param {object} [options]
 * @param {boolean} [options.strict=true] Passer false pour ne pas planter sur des erreurs d'arguments
 * @return {string|string[]|undefined} Si une seule valeur retourne une string, sinon un Array
 */
function getParam (url, name, options) {
  if (!url) return errorHandler('Pas d’url fournie', options)
  if (typeof url !== 'string') return errorHandler('Url fournie invalide', options)
  const [, qs] = split(url)
  const params = querystring.parse(qs) // renvoie toujours un object
  return params[name]
}

/**
 * Retourne tous les paramètres sous forme d'objet
 * @param {string} url absolue ou relative
 * @param {object} [options]
 * @param {boolean} [options.strict=true] Passer false pour ne pas planter sur des erreurs d'arguments
 * @return {object}
 */
function getAllParams (url, options) {
  if (!url) return errorHandler('Pas d’url fournie', options)
  if (typeof url !== 'string') return errorHandler('Url fournie invalide', options)
  const [, qs] = split(url)
  return querystring.parse(qs)
}

/**
 * Retourne l'url absolue de la requête courante
 * @param {Contexte} context
 * @return {string}
 */
function getMyUrl (context) {
  return baseUrl + context.request.originalUrl.substr(1)
}

/**
 * Réuni les morceaux d'une url
 * @param {urlParts} parts
 * @return {string}
 */
function join (parts) {
  let url = parts[0]
  if (parts[1]) url += `?${parts[1]}`
  if (parts[2]) url += `#${parts[2]}`
  return url
}

/**
 * Incrémente skip (et ajoute limit s'il n'y était pas) dans la queryString
 * @param url
 * @param {Object} [options]
 * @param {boolean} [options.replace=false] Passer true pour ne conserver que skip & limit dans la queryString
 * @param {boolean} [options.strict=true] Passer false pour ne pas planter sur des erreurs d'arguments
 * @return {string} L'url de la page suivante
 */
function pageNext (url, options) {
  const params = getAllParams(url, options)
  const { limit, skip } = getNormalizedGrabOptions(params)
  return update(url, { limit, skip: skip + limit }, options)
}
/**
 * Retourne l'url courante avec limit et skip incrémenté
 * @param {Context} context
 */
function pageNextFromContext (context) {
  const myUrl = getMyUrl(context)
  const { limit, skip } = getNormalizedGrabOptions(context.get)
  return update(myUrl, { limit, skip: skip + limit })
}

/**
 * Décrémente skip (et ajoute limit s'il n'y était pas) dans la queryString
 * @param url
 * @param {Object} [options]
 * @param {boolean} [options.replace=false] Passer true pour ne conserver que skip & limit dans la queryString
 * @param {boolean} [options.strict=true] Passer false pour ne pas planter sur des erreurs d'arguments
 * @return {string} L'url de la page précédente (peut être la même si on avait déjà skip = 0)
 */
function pagePrevious (url, options) {
  const params = getAllParams(url, options)
  const { limit, skip } = getNormalizedGrabOptions(params)
  return update(url, { limit, skip: Math.max(0, skip - limit) }, options)
}

/**
 * Retourne l'url courante avec limit et skip décrémenté
 * @param {Context} context
 */
function pagePreviousFromContext (context) {
  const myUrl = getMyUrl(context)
  const { limit, skip } = getNormalizedGrabOptions(context.get)
  return update(myUrl, { limit, skip: Math.max(0, skip - limit) })
}

/**
 * Découpe l'url en 3 morceaux
 * @param {string} url
 * @return {urlParts}
 */
function split (url) {
  if (!url || typeof url !== 'string') return ['', '', '']
  let base, qs
  let [start, anchor] = url.split('#') // si y'a plusieurs # on ignore la fin
  if (start) [base, qs] = start.split('?') // idem
  if (!base) base = ''
  if (!qs) qs = ''
  if (!anchor) anchor = ''
  return [base, qs, anchor]
}

/**
 * Retourne url modifiée pour lui ajouter / mettre à jour des arguments en queryString
 * @param {string} url
 * @param {object} args
 * @param {object} [options]
 * @param {boolean} [options.replace=false] Pour remplacer la querySting avec args (et pas update)
 * @param {boolean} [options.strict=true] Passer false pour ne pas planter sur des erreurs d'arguments
 * @return {string}
 */
function update (url, args, options) {
  if (typeof options !== 'object') options = {}
  const isStrict = hasProp(options, 'strict') ? options.strict : true
  // checks
  if (!url) return errorHandler('Pas d’url fournie', isStrict)
  if (typeof url !== 'string') return errorHandler('Url fournie invalide', isStrict)
  if (!args) return url
  if (typeof args !== 'object') return errorHandler('url.update veut un objet en 2e argument', isStrict, url)
  // args ok mais rien à faire
  if (!options.replace && !Object.keys(args).length) return url
  // faut analyser
  const [base, qs, anchor] = split(url)
  if (options.replace || !qs) return join([base, querystring.stringify(args), anchor])
  // faut un merge
  const params = querystring.parse(qs)
  Object.assign(params, args)
  return join([base, querystring.stringify(params), anchor])
}

module.exports = {
  getParam,
  getAllParams,
  getMyUrl,
  join,
  pageNext,
  pageNextFromContext,
  pagePrevious,
  pagePreviousFromContext,
  split,
  update
}

/**
 * 3 morceaux d'une url
 * @typedef {string[]} urlParts
 * @property {string} urlParts[0] base (absolue ou relative)
 * @property {string} urlParts[1] queryString
 * @property {string} urlParts[2] anchor
 */