Query params

primitive QueryParams
  """
  Builds a URL query string from key-value pairs with RFC 3986
  percent-encoding. Returns an empty string for an empty array, or
  `"?key1=val1&key2=val2"` otherwise.

  Both keys and values are encoded: only unreserved characters (A-Z, a-z,
  0-9, `-`, `.`, `_`, `~`) pass through; everything else becomes `%XX`.
  This includes characters like `&` and `=` that the http library's
  `URLEncode` with `URLPartQuery` would leave unencoded.

  ```pony
  let params = recover val
    [("state", "open"); ("labels", "bug")]
  end
  let query = QueryParams(params) // "?state=open&labels=bug"
  ```
  """

  fun apply(params: Array[(String, String)] val): String val =>
    if params.size() == 0 then
      return ""
    end

    recover val
      let query = String
      var first = true
      for (key, value) in params.values() do
        if first then
          query.append("?")
          first = false
        else
          query.append("&")
        end
        _encode_into(query, key)
        query.append("=")
        _encode_into(query, value)
      end
      query
    end

  fun _encode_into(buf: String ref, value: String) =>
    for byte in value.values() do
      if _is_unreserved(byte) then
        buf.push(byte)
      else
        buf.push('%')
        let hi = (byte >> 4) and 0x0F
        let lo = byte and 0x0F
        buf.push(if hi < 10 then '0' + hi else 'A' + (hi - 10) end)
        buf.push(if lo < 10 then '0' + lo else 'A' + (lo - 10) end)
      end
    end

  fun _is_unreserved(byte: U8): Bool =>
    ((byte >= 'A') and (byte <= 'Z')) or
    ((byte >= 'a') and (byte <= 'z')) or
    ((byte >= '0') and (byte <= '9')) or
    (byte == '-') or (byte == '.') or (byte == '_') or (byte == '~')