src/object/fromQueryString.ts

Summary

Maintainability
D
1 day
Test Coverage
import isNil from '~/is/isNil'
import isNumeric from '~/is/isNumeric'
import type { TextNumber } from '~/internal/types'

const queryRe = /^\?/
const plusRe = /\+/g
const keyRe = /(\[):?([^\]]*)\]/g
const nameRe = /^([^[]+)/ // eslint-disable-line no-useless-escape

type FromQueryStringOptions = {
  decodeName: boolean
}

/**
 * Converts a query string back into an object.
 *
 * Non-recursive:
 *
 *     fromQueryString("foo=1&bar=2"); // returns {foo: '1', bar: '2'}
 *     fromQueryString("foo=&bar=2"); // returns {foo: '', bar: '2'}
 *     fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
 *     fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
 *
 * Recursive:
 *
 *     fromQueryString(
 *         "username=Jacky&"+
 *         "dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&"+
 *         "hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&"+
 *         "hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
 *
 *     // returns
 *     {
 *         username: 'Jacky',
 *         dateOfBirth: {
 *             day: '1',
 *             month: '2',
 *             year: '1911'
 *         },
 *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
 *     }
 *
 * @param {String|null} queryString The query string to decode
 * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
 * @param {Object} options = {
 *   - decodeName {Boolean} Decode KeyNames in the queryString
 * }
 * PHP / Ruby on Rails servers and similar.
 * @return {Object}
 * @todo write tests
 */
export default function fromQueryString(
  queryString: string,
  recursive: boolean = false,
  options: FromQueryStringOptions = { decodeName: true }
): Record<string, any> {
  if (isNil(queryString)) {
    return {}
  }

  let parts = queryString.replace(queryRe, '').split('&'),
    object = Object.create(null),
    temporary,
    components: string[],
    name: string,
    value,
    i,
    ln,
    part: string,
    j,
    subLn,
    matchedKeys: RegExpMatchArray | null,
    matchedName: RegExpMatchArray | null,
    keys: string[],
    key: string,
    nextKey: TextNumber

  for (i = 0, ln = parts.length; i < ln; i++) {
    part = parts[i]

    if (part.length > 0) {
      components = part.split('=')
      name = components[0]
      name = name.replace(plusRe, '%20')
      name = options.decodeName ? decodeURIComponent(name) : name

      value = components[1]

      if (value !== undefined) {
        value = value.replace(plusRe, '%20')
        value = decodeURIComponent(value)
      } else {
        value = ''
      }

      if (!recursive) {
        if (Object.prototype.hasOwnProperty.call(object, name)) {
          if (!Array.isArray(object[name])) {
            object[name] = [object[name]]
          }

          object[name].push(value)
        } else {
          object[name] = value
        }
      } else {
        matchedKeys = name.match(keyRe)
        matchedName = name.match(nameRe)

        //<debug>
        if (!matchedName) {
          throw new Error('[fromQueryString] Malformed query string given, failed parsing name from "' + part + '"')
        }
        //</debug>

        name = matchedName[0]
        keys = []

        if (matchedKeys === null) {
          object[name] = value
          continue
        }

        for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
          key = matchedKeys[j]
          key = key.length === 2 ? '' : key.substring(1, key.length - 1)
          keys.push(key)
        }

        keys.unshift(name)

        temporary = object

        for (j = 0, subLn = keys.length; j < subLn; j++) {
          key = keys[j]

          if (j === subLn - 1) {
            if (Array.isArray(temporary) && key === '') {
              temporary.push(value)
            } else {
              temporary[key] = value
            }
          } else {
            if (temporary[key] === undefined || typeof temporary[key] === 'string') {
              nextKey = keys[j + 1]

              temporary[key] = isNumeric(nextKey) || nextKey === '' ? [] : {}
            }

            temporary = temporary[key]
          }
        }
      }
    }
  }

  return object
}