Current section

11 Versions

Jump to

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 [