Current section

Files

Jump to
raxx lib uri2_query.ex
Raw

lib/uri2_query.ex

defmodule URI2.Query do
@moduledoc """
Utilities for working with url query strings.
Namespaced under `URI2` as it is a replacement for the native `URI` module.
Calls the original module until the query API is finalised.
All functions are pure,
They return try tuples when there is a possibility of failure
"""
@doc """
Returns the query as a list of keyvalue pairs.
This parsing looses no information
## Examples
iex> URI2.Query.parse("foo=1&bar=2")
{:ok, [{"foo", "1"}, {"bar", "2"}]}
iex> URI2.Query.parse("%ZZ")
{:error, :invalid_query}
*Directly using `URI.decode_next_query_pair/1` is not possible as it is private*
"""
def parse(query_string) when is_binary(query_string) do
try do
{:ok, URI.query_decoder(query_string) |> Enum.to_list()}
rescue
_error in ArgumentError ->
{:error, :invalid_query}
end
end
@doc """
Decode a query string into a nested map of values
iex> URI2.Query.decode("percentages[]=15&percentages[]=99+%21")
{:ok, %{"percentages" => ["15", "99 !"]}}
"""
def decode(query_string) when is_binary(query_string) do
case parse(query_string) do
{:ok, key_value_pairs} ->
build_nested(key_value_pairs)
{:error, reason} ->
{:error, reason}
end
end
@doc """
Use bracket notation for nested queries.
Note this is not a formal part of the query specification.
## Examples
iex> URI2.Query.build_nested([{"foo", "1"}, {"bar", "2"}])
{:ok, %{"foo" => "1", "bar" => "2"}}
iex> URI2.Query.build_nested([{"foo[]", "1"}, {"foo[]", "2"}])
{:ok, %{"foo" => ["1", "2"]}}
iex> URI2.Query.build_nested([{"foo[bar]", "1"}, {"foo[baz]", "2"}])
{:ok, %{"foo" => %{"bar" => "1", "baz" => "2"}}}
iex> URI2.Query.build_nested([{"foo[bar][baz]", "1"}])
{:ok, %{"foo" => %{"bar" => %{"baz" => "1"}}}}
iex> URI2.Query.build_nested([{"foo[bar][]", "1"}, {"foo[bar][]", "2"}])
{:ok, %{"foo" => %{"bar" => ["1", "2"]}}}
# I think this case does not work because it is ambiguous whether the second kv item should be added to the first list item.
# iex> URI2.Query.build_nested([{"foo[][bar]", "1"}, {"foo[][baz]", "2"}])
# {:ok, %{"foo" => [%{"bar" => "1"}, %{"baz" => "2"}]}}
"""
def build_nested(key_value_pairs, nested \\ %{})
def build_nested([], nested) do
{:ok, nested}
end
def build_nested([{key, value} | key_value_pairs], nested) do
updated = case :binary.split(key, "[") do
[key] ->
put_single_value(nested, key, value)
[key, "]"] ->
put_array_entry(nested, key, value)
[key, rest] ->
case :binary.split(rest, "]") do
[subkey, rest] ->
put_sub_query(nested, key, [{subkey <> rest, value}])
end
end
case updated do
{:ok, nested} ->
build_nested(key_value_pairs, nested)
{:error, reason} ->
{:error, reason}
end
end
defp put_sub_query(map, key, key_value_pairs) do
case Map.get(map, key, %{}) do
subquery = %{} ->
{:ok, subquery} = build_nested(key_value_pairs, subquery)
{:ok, Map.put(map, key, subquery)}
other ->
{:error, {:key_already_defined_as, other}}
end
end
defp put_single_value(map, key, value) do
case Map.has_key?(map, key) do
false ->
{:ok, Map.put_new(map, key, value)}
true ->
{:error, {:key_already_defined_as, Map.get(map, key)}}
end
end
defp put_array_entry(map, key, value) do
case Map.get(map, key, []) do
values when is_list(values) ->
{:ok, Map.put(map, key, values ++ [value])}
other ->
{:error, {:key_already_defined_as, other}}
end
end
end