Current section
11 Versions
Jump to
Current section
11 Versions
Compare versions
5
files changed
+320
additions
-205
deletions
| @@ -2,6 +2,16 @@ | |
| 2 2 | |
| 3 3 | All notable changes to this project will be documented in this file. |
| 4 4 | |
| 5 | + ## [0.2.0] - 2025-11-04 |
| 6 | + |
| 7 | + ### 🚜 Refactor |
| 8 | + |
| 9 | + - Rewrote the UriTemplate rendered |
| 10 | + |
| 11 | + ### 📚 Documentation |
| 12 | + |
| 13 | + - Fix doc examples |
| 14 | + |
| 5 15 | ## [0.1.0] - 2025-09-15 |
| 6 16 | |
| 7 17 | ### 🚀 Features |
| @@ -3,7 +3,7 @@ | |
| 3 3 | <<"https://github.com/lud/texture/blob/main/CHANGELOG.md">>}, |
| 4 4 | {<<"Github">>,<<"https://github.com/lud/texture">>}]}. |
| 5 5 | {<<"name">>,<<"texture">>}. |
| 6 | - {<<"version">>,<<"0.1.0">>}. |
| 6 | + {<<"version">>,<<"0.2.0">>}. |
| 7 7 | {<<"description">>,<<"A collection of structured text parsers.">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.15">>}. |
| 9 9 | {<<"app">>,<<"texture">>}. |
| @@ -44,74 +44,61 @@ defmodule Texture.HttpStructuredField do | |
| 44 44 | |
| 45 45 | An item with no parameters: |
| 46 46 | |
| 47 | - iex> Texture.HttpStructuredField.parse_item("123") |
| 48 | - {:ok, {:integer, 123, []}} |
| 47 | + iex> Texture.HttpStructuredField.parse_item("123") |
| 48 | + {:ok, {:integer, 123, []}} |
| 49 49 | |
| 50 50 | Item with boolean (implicit) and integer parameters: |
| 51 51 | |
| 52 52 | iex> Texture.HttpStructuredField.parse_item("123;a;b=5") |
| 53 | - {:ok, {:integer, 123, [ |
| 54 | - {"a", {:boolean, true}}, |
| 55 | - {"b", {:integer, 5}} |
| 56 | - ]}} |
| 53 | + {:ok, {:integer, 123, [{"a", {:boolean, true}}, {"b", {:integer, 5}}]}} |
| 57 54 | |
| 58 55 | Unwrapped (type tags removed): |
| 59 56 | |
| 60 57 | iex> Texture.HttpStructuredField.parse_item("123;a;b=5", unwrap: true) |
| 61 | - {:ok, {123, [ |
| 62 | - {"a", true}, |
| 63 | - {"b", 5} |
| 64 | - ]}} |
| 58 | + {:ok, {123, [{"a", true}, {"b", 5}]}} |
| 65 59 | |
| 66 60 | Attributes as a map (still wrapped): |
| 67 61 | |
| 68 | - iex> Texture.HttpStructuredField.parse_item("123;a;b=5", maps: true) |
| 69 | - {:ok, {:integer, 123, %{"a" => {:boolean, true}, "b" => {:integer, 5}}}} |
| 62 | + iex> Texture.HttpStructuredField.parse_item("123;a;b=5", maps: true) |
| 63 | + {:ok, {:integer, 123, %{"a" => {:boolean, true}, "b" => {:integer, 5}}}} |
| 70 64 | |
| 71 65 | Both together (unwrapped values and attribute map): |
| 72 66 | |
| 73 | - iex> Texture.HttpStructuredField.parse_item("123;a;b=5", unwrap: true, maps: true) |
| 74 | - {:ok, {123, %{"a" => true, "b" => 5}}} |
| 67 | + iex> Texture.HttpStructuredField.parse_item("123;a;b=5", unwrap: true, maps: true) |
| 68 | + {:ok, {123, %{"a" => true, "b" => 5}}} |
| 75 69 | |
| 76 70 | ## Parsing a list |
| 77 71 | |
| 78 72 | A list can contain bare items and inner lists: |
| 79 73 | |
| 80 74 | iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p") |
| 81 | - {:ok, [ |
| 82 | - {:integer, 123, []}, |
| 83 | - {:string, "hi", [{"a", {:integer, 1}}]}, |
| 84 | - {:inner_list, [ |
| 85 | - {:integer, 1, []}, |
| 86 | - {:integer, 2, []}, |
| 87 | - {:integer, 3, []} |
| 88 | - ], [{"p", {:boolean, true}}]} |
| 75 | + {:ok, |
| 76 | + [ |
| 77 | + {:integer, 123, []}, |
| 78 | + {:string, "hi", [{"a", {:integer, 1}}]}, |
| 79 | + {:inner_list, |
| 80 | + [{:integer, 1, []}, {:integer, 2, []}, {:integer, 3, []}], |
| 81 | + [{"p", {:boolean, true}}]} |
| 89 82 | ]} |
| 90 83 | |
| 91 84 | Unwrapping removes all type tags recursively: |
| 92 85 | |
| 93 86 | iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p", unwrap: true) |
| 94 | - {:ok, [ |
| 95 | - {123, []}, |
| 96 | - {"hi", [{"a", 1}]}, |
| 97 | - {[ |
| 98 | - {1, []}, |
| 99 | - {2, []}, |
| 100 | - {3, []} |
| 101 | - ], [{"p", true}]} |
| 87 | + {:ok, |
| 88 | + [ |
| 89 | + {123, []}, |
| 90 | + {"hi", [{"a", 1}]}, |
| 91 | + {[{1, []}, {2, []}, {3, []}], [{"p", true}]} |
| 102 92 | ]} |
| 103 93 | |
| 104 94 | Using maps for attributes (note inner list parameter map): |
| 105 95 | |
| 106 96 | iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p", unwrap: true, maps: true) |
| 107 | - {:ok, [ |
| 108 | - {123, %{}}, |
| 109 | - {"hi", %{"a" => 1}}, |
| 110 | - {[ |
| 111 | - {1, %{}}, |
| 112 | - {2, %{}}, |
| 113 | - {3, %{}}, |
| 114 | - ], %{"p" => true}} |
| 97 | + {:ok, |
| 98 | + [ |
| 99 | + {123, %{}}, |
| 100 | + {"hi", %{"a" => 1}}, |
| 101 | + {[{1, %{}}, {2, %{}}, {3, %{}}], %{"p" => true}} |
| 115 102 | ]} |
| 116 103 | |
| 117 104 | ## Parsing a dictionary |
| @@ -119,64 +106,56 @@ defmodule Texture.HttpStructuredField do | |
| 119 106 | Example with explicit and implicit boolean members plus inner list: |
| 120 107 | |
| 121 108 | iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p") |
| 122 | - {:ok, [ |
| 123 | - {"foo", {:integer, 123, []}}, |
| 124 | - {"bar", {:boolean, true, []}}, |
| 125 | - {"baz", {:string, "hi", [ |
| 126 | - {"a", {:integer, 1}}, |
| 127 | - {"b", {:integer, 2}} |
| 128 | - ]}}, |
| 129 | - {"qux", {:inner_list, [ |
| 130 | - {:integer, 1, []}, |
| 131 | - {:integer, 2, []} |
| 132 | - ], [{"p", {:boolean, true}}]}} |
| 109 | + {:ok, |
| 110 | + [ |
| 111 | + {"foo", {:integer, 123, []}}, |
| 112 | + {"bar", {:boolean, true, []}}, |
| 113 | + {"baz", {:string, "hi", [{"a", {:integer, 1}}, {"b", {:integer, 2}}]}}, |
| 114 | + {"qux", |
| 115 | + {:inner_list, [{:integer, 1, []}, {:integer, 2, []}], |
| 116 | + [{"p", {:boolean, true}}]}} |
| 133 117 | ]} |
| 134 118 | |
| 135 119 | Unwrapped: |
| 136 120 | |
| 137 121 | iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", unwrap: true) |
| 138 | - {:ok, [ |
| 139 | - {"foo", {123, []}}, |
| 140 | - {"bar", {true, []}}, |
| 141 | - {"baz", {"hi", [{"a", 1}, {"b", 2}]}}, |
| 142 | - {"qux", {[ |
| 143 | - {1, []}, |
| 144 | - {2, []} |
| 145 | - ], [{"p", true}]}} |
| 122 | + {:ok, |
| 123 | + [ |
| 124 | + {"foo", {123, []}}, |
| 125 | + {"bar", {true, []}}, |
| 126 | + {"baz", {"hi", [{"a", 1}, {"b", 2}]}}, |
| 127 | + {"qux", {[{1, []}, {2, []}], [{"p", true}]}} |
| 146 128 | ]} |
| 147 129 | |
| 148 130 | As a map (still wrapped): |
| 149 131 | |
| 150 132 | iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", maps: true) |
| 151 | - {:ok, %{ |
| 152 | - "foo" => {:integer, 123, %{}}, |
| 153 | - "bar" => {:boolean, true, %{}}, |
| 154 | - "baz" => {:string, "hi", %{"a" => {:integer, 1}, "b" => {:integer, 2}}}, |
| 155 | - "qux" => {:inner_list, [ |
| 156 | - {:integer, 1, %{}}, |
| 157 | - {:integer, 2, %{}}, |
| 158 | - ], %{"p" => {:boolean, true}}} |
| 133 | + {:ok, |
| 134 | + %{ |
| 135 | + "bar" => {:boolean, true, %{}}, |
| 136 | + "baz" => {:string, "hi", %{"a" => {:integer, 1}, "b" => {:integer, 2}}}, |
| 137 | + "foo" => {:integer, 123, %{}}, |
| 138 | + "qux" => {:inner_list, [{:integer, 1, %{}}, {:integer, 2, %{}}], |
| 139 | + %{"p" => {:boolean, true}}} |
| 159 140 | }} |
| 160 141 | |
| 161 142 | Maps + Unwrapped: |
| 162 143 | |
| 163 144 | iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", unwrap: true, maps: true) |
| 164 | - {:ok, %{ |
| 165 | - "foo" => {123, %{}}, |
| 166 | - "bar" => {true, %{}}, |
| 167 | - "baz" => {"hi", %{"a" => 1, "b" => 2}}, |
| 168 | - "qux" => {[ |
| 169 | - {1, %{}}, |
| 170 | - {2, %{}}, |
| 171 | - ], %{"p" => true}} |
| 145 | + {:ok, |
| 146 | + %{ |
| 147 | + "bar" => {true, %{}}, |
| 148 | + "baz" => {"hi", %{"a" => 1, "b" => 2}}, |
| 149 | + "foo" => {123, %{}}, |
| 150 | + "qux" => {[{1, %{}}, {2, %{}}], %{"p" => true}} |
| 172 151 | }} |
| 173 152 | |
| 174 153 | ## Error handling |
| 175 154 | |
| 176 155 | On invalid input an `{:error, {reason, remainder}}` tuple is returned: |
| 177 156 | |
| 178 | - iex> Texture.HttpStructuredField.parse_item("not@@valid") |
| 179 | - {:error, {:invalid_value, "not@@valid"}} |
| 157 | + iex> Texture.HttpStructuredField.parse_item("not@@valid") |
| 158 | + {:error, {:invalid_value, "not@@valid"}} |
| 180 159 | |
| 181 160 | The low-level tokenization lives in the private `Parser` module; only the |
| 182 161 | post-processing (unwrap / maps) occurs here. |
| @@ -76,25 +76,76 @@ defmodule Texture.UriTemplate do | |
| 76 76 | unbox: ["URI-Template", "varchar", "op-level2", "op-level3", "op-reserve", "modifier-level4"], |
| 77 77 | unwrap: ["literals", "explode"], |
| 78 78 | untag: ["max-length"], |
| 79 | - ignore: [] |
| 79 | + ignore: [], |
| 80 | + private: true |
| 80 81 | |
| 81 82 | @type t :: %__MODULE__{parts: term, raw: binary} |
| 82 83 | |
| 83 | - @doc false |
| 84 | + @doc """ |
| 85 | + Parses an URI template into an internal representation. |
| 86 | + """ |
| 84 87 | @spec parse(binary) :: {:ok, t} | {:error, term} |
| 85 88 | def parse(data) do |
| 86 89 | case uri_template(data) do |
| 87 | - {:ok, parts, "", _, _, _} -> {:ok, %__MODULE__{parts: parts, raw: data}} |
| 90 | + {:ok, parts, "", _, _, _} -> {:ok, %__MODULE__{parts: post_parse(parts), raw: data}} |
| 88 91 | {:ok, _, rest, _, _, _} -> {:error, {:invalid_value, rest}} |
| 89 92 | end |
| 90 93 | end |
| 91 94 | |
| 95 | + defp post_parse(parts) do |
| 96 | + Enum.map(parts, &post_parse_part/1) |
| 97 | + end |
| 98 | + |
| 99 | + defp post_parse_part(part) do |
| 100 | + case part do |
| 101 | + {:literals, _} = lit -> |
| 102 | + lit |
| 103 | + |
| 104 | + {:expression, ["{" | expr]} -> |
| 105 | + {"}", expr} = List.pop_at(expr, -1) |
| 106 | + |
| 107 | + {op, varlist} = |
| 108 | + case expr do |
| 109 | + [operator: [op], variable_list: varlist] -> {op, post_parse_varlist(varlist)} |
| 110 | + [variable_list: varlist] -> {:default, post_parse_varlist(varlist)} |
| 111 | + end |
| 112 | + |
| 113 | + {:expr, op, varlist} |
| 114 | + end |
| 115 | + end |
| 116 | + |
| 117 | + defp post_parse_varlist(elems) do |
| 118 | + Enum.flat_map(elems, fn |
| 119 | + "," -> |
| 120 | + [] |
| 121 | + |
| 122 | + {:varspec, [varname: varname]} -> |
| 123 | + [{:var, Enum.join(varname), nil}] |
| 124 | + |
| 125 | + {:varspec, [varname: varname, explode: "*"]} -> |
| 126 | + [{:var, Enum.join(varname), :explode}] |
| 127 | + |
| 128 | + {:varspec, [varname: varname, prefix: [":", n_aslist]]} -> |
| 129 | + {max_len, ""} = Integer.parse(List.to_string(n_aslist)) |
| 130 | + [{:var, Enum.join(varname), {:prefix, max_len}}] |
| 131 | + end) |
| 132 | + end |
| 133 | + |
| 134 | + @doc """ |
| 135 | + Renders a template given its internal representation and a map of parameters. |
| 136 | + |
| 137 | + Rendering has partial support for list of tuples. Such lists will be rendered |
| 138 | + as maps, but empty lists are still condireded undefined values. |
| 139 | + |
| 140 | + Also note that literal parts of the template (everything that is not in `{` |
| 141 | + `}` will be returned as-is, whereas it should be percent-encoded. |
| 142 | + """ |
| 92 143 | @spec render(t, %{optional(atom) => term, optional(binary) => term}) :: binary |
| 93 144 | def render(%__MODULE__{parts: parts}, params) do |
| 94 145 | params = |
| 95 146 | Map.new(params, fn |
| 96 147 | {key, value} when is_binary(key) -> {key, value} |
| 97 | - {key, value} when is_atom(key) -> {render_key(key), value} |
| 148 | + {key, value} when is_atom(key) -> {Atom.to_string(key), value} |
| 98 149 | end) |
| 99 150 | |
| 100 151 | IO.iodata_to_binary(do_render(parts, params)) |
| @@ -104,168 +155,243 @@ defmodule Texture.UriTemplate do | |
| 104 155 | Enum.map(parts, fn item -> render_part(item, params) end) |
| 105 156 | end |
| 106 157 | |
| 107 | - defp render_part({:literals, n}, _) do |
| 108 | - n |
| 109 | - end |
| 110 | - |
| 111 | - defp render_part({:expression, ["{", {:variable_list, varlist}, "}"]}, params) do |
| 112 | - Enum.map(varlist, fn {:varspec, vspec} -> render_var(vspec, params, :allow_unreserved) end) |
| 113 | - end |
| 114 | - |
| 115 | - defp render_part( |
| 116 | - {:expression, ["{", {:operator, [op]}, {:variable_list, varlist}, "}"]}, |
| 117 | - params |
| 118 | - ) do |
| 119 | - render_variable_list_kv(op, varlist, params) |
| 120 | - end |
| 121 | - |
| 122 | - defp render_variable_list_kv(";", varlist, params) do |
| 123 | - varlist |
| 124 | - |> Enum.flat_map(fn |
| 125 | - {:varspec, vspec} -> render_kvs(vspec, params, _enforce_eq? = false, :allow_unreserved) |
| 126 | - "," -> [] |
| 127 | - end) |
| 128 | - |> Enum.map(&[";", &1]) |
| 129 | - end |
| 130 | - |
| 131 | - defp render_variable_list_kv(op, varlist, params) when op in ["?", "&"] do |
| 132 | - varlist |
| 133 | - |> Enum.flat_map(fn |
| 134 | - {:varspec, vspec} -> render_kvs(vspec, params, _enforce_eq? = true, :allow_unreserved) |
| 135 | - "," -> [] |
| 136 | - end) |
| 137 | - |> Enum.intersperse([?&]) |
| 138 | - |> prefix_nonempty_list(op) |
| 139 | - end |
| 140 | - |
| 141 | - defp render_variable_list_kv("#", varlist, params) do |
| 142 | - varlist |
| 143 | - |> Enum.flat_map(fn |
| 144 | - {:varspec, vspec} -> render_var(vspec, params, :allow_reserved_unreserved) |
| 145 | - "," -> [] |
| 146 | - end) |
| 147 | - |> Enum.intersperse(?,) |
| 148 | - |> prefix_nonempty_list(?#) |
| 149 | - end |
| 150 | - |
| 151 | - defp render_variable_list_kv("+", varlist, params) do |
| 152 | - varlist |
| 153 | - |> Enum.flat_map(fn |
| 154 | - {:varspec, vspec} -> render_var(vspec, params, :allow_reserved_unreserved) |
| 155 | - "," -> [] |
| 156 | - end) |
| 157 | - |> Enum.intersperse(?,) |
| 158 | - end |
| 159 | - |
| 160 | - defp render_variable_list_kv(op, varlist, params) when op in ["/", "."] do |
| 161 | - varlist |
| 162 | - |> Enum.flat_map(fn |
| 163 | - {:varspec, vspec} -> render_var(vspec, params, :allow_unreserved) |
| 164 | - "," -> [] |
| 165 | - end) |
| 166 | - |> Enum.intersperse(op) |
| 167 | - |> prefix_nonempty_list(op) |
| 168 | - end |
| 169 | - |
| 170 | - defp prefix_nonempty_list([], _) do |
| 171 | - [] |
| 172 | - end |
| 173 | - |
| 174 | - defp prefix_nonempty_list(list, prefix) do |
| 175 | - [prefix, list] |
| 176 | - end |
| 177 | - |
| 178 | - defp render_kvs([{:varname, letters}], params, enforce_eq?, special_chars) do |
| 179 | - name = Enum.join(letters) |
| 180 | - |
| 181 | - case Map.fetch(params, name) do |
| 182 | - {:ok, []} -> [] |
| 183 | - {:ok, value} -> [render_kv(name, enforce_eq?, render_value(value, special_chars))] |
| 184 | - :error -> [] |
| 158 | + defp render_part({:literals, lit}, _) do |
| 159 | + case lit do |
| 160 | + c when is_integer(c) -> c |
| 161 | + {:ucschar, [c]} -> <<c::utf8>> |
| 162 | + {:iprivate, [c]} -> <<c::utf8>> |
| 163 | + {:pct_encoded, graphemes} -> graphemes |
| 185 164 | end |
| 186 165 | end |
| 187 166 | |
| 188 | - defp render_kvs([{:varname, letters}, {:explode, "*"}], params, enforce_eq?, special_chars) do |
| 189 | - name = Enum.join(letters) |
| 167 | + defp render_part({:expr, op, varlist}, params) do |
| 168 | + render_expr(op, varlist, params) |
| 169 | + end |
| 190 170 | |
| 191 | - case Map.fetch!(params, name) do |
| 192 | - list when is_list(list) -> |
| 193 | - Enum.map(list, &[render_kv(name, enforce_eq?, render_value(&1, special_chars))]) |
| 171 | + defp render_expr(op, varlist, params) do |
| 172 | + # dict_mode |
| 173 | + # * :always => turn lists and map into key=value strings, with a `=` for empty values, like a=&b=2 |
| 174 | + # * :nonempty => turn lists and map into key=value strings, without a `=` for empty values like a&b=2 |
| 175 | + # * :dicts => do not encode as dict, return "2", EXCEPT for map or keywords |
| 194 176 | |
| 195 | - map when map_size(map) > 0 -> |
| 196 | - Enum.map(map, fn {key, value} -> |
| 197 | - [render_kv(render_key(key), enforce_eq?, render_value(value, special_chars))] |
| 198 | - end) |
| 177 | + {escape, dict_mode, c_intersperse, c_listprefix} = |
| 178 | + case op do |
| 179 | + c when c in [";"] -> {:allow_unreserved, :nonempty, op, op} |
| 180 | + c when c in ["?", "&"] -> {:allow_unreserved, :always, ?&, op} |
| 181 | + c when c in ["/", "."] -> {:allow_unreserved, :dicts, op, op} |
| 182 | + "#" -> {:allow_reserved_unreserved, :dicts, ?,, ?#} |
| 183 | + "+" -> {:allow_reserved_unreserved, :dicts, ?,, nil} |
| 184 | + :default -> {:allow_unreserved, :dicts, ?,, nil} |
| 185 | + end |
| 199 186 | |
| 200 | - %{} -> |
| 187 | + values = render_varlist(varlist, escape, dict_mode, params) |
| 188 | + |
| 189 | + values = Enum.intersperse(values, c_intersperse) |
| 190 | + |
| 191 | + case values do |
| 192 | + [_ | _] when c_listprefix != nil -> [c_listprefix | values] |
| 193 | + _ -> values |
| 194 | + end |
| 195 | + end |
| 196 | + |
| 197 | + # With those operators we will include the keys in the rendered vars |
| 198 | + defp render_varlist(varlist, escape, dict_mode, params) do |
| 199 | + Enum.flat_map(varlist, &render_var(&1, escape, dict_mode, params)) |
| 200 | + end |
| 201 | + |
| 202 | + # Render var always returns a list, so we can merge normal values and explode* |
| 203 | + # values in a list at the same level (for further intersperse) |
| 204 | + |
| 205 | + defp render_var({:var, name, :explode}, escape, dict_mode, params) do |
| 206 | + case fetch_param(params, name) do |
| 207 | + {:ok, value} -> |
| 208 | + value |
| 209 | + |> explode_value(dict_mode, name) |
| 210 | + |> render_pairs(escape, dict_mode) |
| 211 | + |
| 212 | + :error -> |
| 201 213 | [] |
| 202 214 | end |
| 203 215 | end |
| 204 216 | |
| 205 | - defp render_kv(name, true, "") do |
| 206 | - [name, ?=] |
| 207 | - end |
| 208 | - |
| 209 | - defp render_kv(name, false, "") do |
| 210 | - name |
| 211 | - end |
| 212 | - |
| 213 | - defp render_kv(name, _, rendered_value) do |
| 214 | - [name, ?=, rendered_value] |
| 215 | - end |
| 216 | - |
| 217 | - defp render_var([{:varname, letters}], params, special_chars) do |
| 218 | - name = Enum.join(letters) |
| 219 | - |
| 220 | - case Map.fetch(params, name) do |
| 221 | - {:ok, value} -> [render_value(value, special_chars)] |
| 217 | + defp render_var({:var, name, nil}, escape, dict_mode, params) do |
| 218 | + case fetch_param(params, name) do |
| 219 | + {:ok, value} -> render_pairs([{name, value}], escape, dict_mode) |
| 222 220 | :error -> [] |
| 223 221 | end |
| 224 222 | end |
| 225 223 | |
| 226 | - defp render_var([{:varname, letters}, {:explode, "*"}], params, special_chars) do |
| 227 | - name = Enum.join(letters) |
| 228 | - |
| 229 | - case Map.fetch!(params, name) do |
| 230 | - list when is_list(list) -> Enum.map(list, &render_value(&1, special_chars)) |
| 224 | + defp render_var({:var, name, {:prefix, max_len}}, escape, dict_mode, params) do |
| 225 | + case fetch_param(params, name) do |
| 226 | + {:ok, value} -> render_pairs([{name, value}], {escape, max_len}, dict_mode) |
| 227 | + :error -> [] |
| 231 228 | end |
| 232 229 | end |
| 233 230 | |
| 234 | - defp render_var([{:varname, letters}, {:prefix, [_, n_aslist]}], params, special_chars) do |
| 235 | - {max_len, ""} = Integer.parse(List.to_string(n_aslist)) |
| 236 | - name = Enum.join(letters) |
| 231 | + defp fetch_param(params, key) do |
| 232 | + # https://www.rfc-editor.org/rfc/rfc6570.html#section-2.3 |
| 233 | + # |
| 234 | + # * A variable defined as a list value is considered undefined if the list |
| 235 | + # contains zero members. |
| 236 | + # * A variable defined as an associative array of (name, value) pairs is |
| 237 | + # considered undefined if the array contains zero members or if all member |
| 238 | + # names in the array are associated with undefined values. |
| 239 | + case Map.fetch(params, key) do |
| 240 | + {:ok, list} when is_list(list) -> |
| 241 | + check_undef_compound(list) |
| 237 242 | |
| 238 | - case Map.fetch!(params, name) do |
| 239 | - value -> [render_value(value, {special_chars, max_len})] |
| 243 | + {:ok, map} when is_map(map) -> |
| 244 | + check_undef_compound(map) |
| 245 | + |
| 246 | + {:ok, value} -> |
| 247 | + case undef?(value) do |
| 248 | + true -> :error |
| 249 | + false -> {:ok, value} |
| 250 | + end |
| 251 | + |
| 252 | + :error -> |
| 253 | + :error |
| 240 254 | end |
| 241 255 | end |
| 242 256 | |
| 243 | - defp render_value(list, special_chars) when is_list(list) do |
| 244 | - Enum.map_intersperse(list, ?,, &render_value(&1, special_chars)) |
| 257 | + defp check_undef_compound(list) when is_list(list) do |
| 258 | + case Enum.reject(list, &undef?/1) do |
| 259 | + [] -> :error |
| 260 | + list -> {:ok, list} |
| 261 | + end |
| 245 262 | end |
| 246 263 | |
| 247 | - defp render_value(value, special_chars) do |
| 264 | + defp check_undef_compound(map) when is_map(map) do |
| 265 | + case Map.reject(map, fn {_, v} -> undef?(v) end) do |
| 266 | + empty when map_size(empty) == 0 -> :error |
| 267 | + %_{} = empty when map_size(empty) == 1 -> :error |
| 268 | + map -> {:ok, map} |
| 269 | + end |
| 270 | + end |
| 271 | + |
| 272 | + defp undef?(nil) do |
| 273 | + true |
| 274 | + end |
| 275 | + |
| 276 | + defp undef?([]) do |
| 277 | + true |
| 278 | + end |
| 279 | + |
| 280 | + defp undef?(empty_map) when empty_map == %{} do |
| 281 | + true |
| 282 | + end |
| 283 | + |
| 284 | + defp undef?(_) do |
| 285 | + false |
| 286 | + end |
| 287 | + |
| 288 | + defp explode_value(list, :dicts, _default_key) when is_list(list) do |
| 289 | + Enum.map(list, fn |
| 290 | + {k, v} -> {:pair, k, v} |
| 291 | + v -> v |
| 292 | + end) |
| 293 | + end |
| 294 | + |
| 295 | + defp explode_value(list, _, default_key) when is_list(list) do |
| 296 | + Enum.map(list, fn |
| 297 | + {k, v} -> {k, v} |
| 298 | + v -> {default_key, v} |
| 299 | + end) |
| 300 | + end |
| 301 | + |
| 302 | + defp explode_value(map, _, _default_key) when is_map(map) do |
| 303 | + Map.to_list(map) |
| 304 | + end |
| 305 | + |
| 306 | + defp render_pairs(list, escape, dict_mode) when is_list(list) do |
| 307 | + case dict_mode do |
| 308 | + :dicts -> |
| 309 | + Enum.flat_map(list, fn |
| 310 | + {_k, v} -> [render_value(v, escape)] |
| 311 | + {:pair, k, v} -> [[render_key(k), ?=, render_value(v, escape)]] |
| 312 | + v -> [render_value(v, escape)] |
| 313 | + end) |
| 314 | + |
| 315 | + :always -> |
| 316 | + Enum.map(list, fn {k, v} -> [render_kv(k, v, escape, :enforce_sep)] end) |
| 317 | + |
| 318 | + :nonempty -> |
| 319 | + Enum.map(list, fn {k, v} -> [render_kv(k, v, escape, :nonempty)] end) |
| 320 | + end |
| 321 | + end |
| 322 | + |
| 323 | + defp render_kv(k, v, escape, :enforce_sep) do |
| 324 | + [render_key(k), ?=, render_value(v, escape)] |
| 325 | + end |
| 326 | + |
| 327 | + defp render_kv(k, v, escape, :nonempty) do |
| 328 | + case render_value(v, escape) do |
| 329 | + "" -> render_key(k) |
| 330 | + str -> [render_key(k), ?=, str] |
| 331 | + end |
| 332 | + end |
| 333 | + |
| 334 | + defp render_value(list, escape) when is_list(list) do |
| 335 | + Enum.map_intersperse(list, ?,, fn |
| 336 | + # Support for keyword list |
| 337 | + {k, v} -> |
| 338 | + v = |
| 339 | + v |
| 340 | + |> value_to_string() |
| 341 | + |> encode_value(escape) |
| 342 | + |
| 343 | + [render_key(k), ?,, v] |
| 344 | + |
| 345 | + v -> |
| 346 | + v |
| 347 | + |> value_to_string() |
| 348 | + |> encode_value(escape) |
| 349 | + end) |
| 350 | + end |
| 351 | + |
| 352 | + defp render_value(map, escape) when is_map(map) do |
| 353 | + Enum.map_intersperse(map, ?,, fn {k, v} -> |
| 354 | + v = |
| 355 | + v |
| 356 | + |> value_to_string() |
| 357 | + |> encode_value(escape) |
| 358 | + |
| 359 | + [render_key(k), ?,, v] |
| 360 | + end) |
| 361 | + end |
| 362 | + |
| 363 | + defp render_value(value, {escape, max_len}) do |
| 248 364 | value |
| 249 365 | |> value_to_string() |
| 250 | - |> encode_value(special_chars) |
| 366 | + |> String.slice(0..(max_len - 1)) |
| 367 | + |> encode_value(escape) |
| 368 | + end |
| 369 | + |
| 370 | + defp render_value(value, escape) do |
| 371 | + value |
| 372 | + |> value_to_string() |
| 373 | + |> encode_value(escape) |
| 251 374 | end |
| 252 375 | |
| 253 376 | defp value_to_string(str) when is_binary(str) do |
| 254 377 | str |
| 255 378 | end |
| 256 379 | |
| 380 | + defp value_to_string(nil) do |
| 381 | + "" |
| 382 | + end |
| 383 | + |
| 257 384 | defp value_to_string(atom) when is_atom(atom) do |
| 258 | - Atom.to_string(atom) |
| 385 | + case Atom.to_string(atom) do |
| 386 | + "Elixir." <> rest -> rest |
| 387 | + all -> all |
| 388 | + end |
| 259 389 | end |
| 260 390 | |
| 261 391 | defp value_to_string(other) do |
| 262 392 | Kernel.to_string(other) |
| 263 393 | end |
| 264 394 | |
| 265 | - defp encode_value(value, {special_chars, max_len}) do |
| 266 | - encode_value(String.slice(value, 0, max_len), special_chars) |
| 267 | - end |
| 268 | - |
| 269 395 | defp encode_value(value, :allow_unreserved) do |
| 270 396 | URI.encode(value, &URI.char_unreserved?/1) |
| 271 397 | end |
| @@ -2,7 +2,7 @@ defmodule Texture.MixProject do | |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 4 | @source_url "https://github.com/lud/texture" |
| 5 | - @version "0.1.0" |
| 5 | + @version "0.2.0" |
| 6 6 | |
| 7 7 | def project do |
| 8 8 | [ |