Skip to content

Commit 3fa31a6

Browse files
committed
Merge branch 'master' of github.com:vic/params
2 parents c9fea01 + 8c9367a commit 3fa31a6

File tree

3 files changed

+131
-25
lines changed

3 files changed

+131
-25
lines changed

README.md

+23-4
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,6 @@ required fields by ending them with a `!`, of course
192192
the bang is removed from the field definition and is
193193
only used to mark which fields are required by default.
194194

195-
The [Params.data](http://hexdocs.pm/params/Params.html#data/1)
196-
and [Params.changes](http://hexdocs.pm/params/Params.html#changes/1) can be useful
197-
for obtaining an struct or map from a changeset.
198-
199195
You can also create a module and define
200196
your schema or custom changesets in it:
201197

@@ -220,6 +216,29 @@ defmodule MyApp.UserController do
220216
end
221217
```
222218

219+
The [Params.data](http://hexdocs.pm/params/Params.html#data/1)
220+
and [Params.to_map](http://hexdocs.pm/params/Params.html#to_map/1) can be useful
221+
for obtaining a struct or map from a changeset.
222+
223+
Note that `Params.data` and `Params.to_map` have different behaviour: `data`
224+
returns a struct which will include all valid params. `to_map` returns a map
225+
that only includes the submitted keys and keys with default values:
226+
227+
```elixir
228+
defmodule UserUpdateParams do
229+
use Params.Schema, %{
230+
name: :string,
231+
age: :integer,
232+
auditlog: [field: :boolean, default: true]
233+
}
234+
end
235+
236+
changeset = UserUpdateParams.from(%{name: "John"})
237+
238+
Params.data(changeset) # => %UserUpdateParams{name: "John", age: nil, auditlog: true}
239+
Params.to_map(changeset) # => %{name: "John", auditlog: true}
240+
```
241+
223242
## API Documentation
224243

225244
[API Documentation](https://hexdocs.pm/params/)

lib/params.ex

+50-15
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ defmodule Params do
4040
@doc """
4141
Transforms an Ecto.Changeset into a Map with atom keys.
4242
43-
Recursively traverses and transforms embedded changesets and skips the _id
44-
primary key field.
43+
Recursively traverses and transforms embedded changesets and skips keys that
44+
was not part of params given to changeset
4545
"""
4646
@spec to_map(Changeset.t) :: map
47-
def to_map(%Changeset{} = ch) do
48-
ch
49-
|> data
50-
|> Map.from_struct
51-
|> sanitize_map
47+
def to_map(%Changeset{data: %{__struct__: module}} = ch) do
48+
default = module |> schema |> defaults
49+
change = changes(ch)
50+
51+
deep_merge(default, change)
5252
end
5353

5454
@doc """
@@ -183,14 +183,49 @@ defmodule Params do
183183
end)
184184
end
185185

186-
defp sanitize_map(%{} = map) do
187-
Enum.reduce(map, %{}, fn {k, v}, m ->
188-
case {k, v} do
189-
{:__meta__, _} -> m
190-
{:_id, _} -> m
191-
{k, %{__struct__: _} = struct} -> Map.put(m, k, struct |> Map.from_struct |> sanitize_map)
192-
{k, %{} = nested} -> Map.put(m, k, sanitize_map(nested))
193-
{k, v} -> Map.put(m, k, v)
186+
defp deep_merge(%{} = map_1, %{} = map_2) do
187+
Map.merge(map_1, map_2, &deep_merge_conflict/3)
188+
end
189+
190+
defp deep_merge_conflict(_k, %{} = m1, %{} = m2) do
191+
deep_merge(m1, m2)
192+
end
193+
defp deep_merge_conflict(_k, _v1, v2), do: v2
194+
195+
defp defaults(params, acc \\ %{}, path \\ [])
196+
defp defaults([], acc, _path), do: acc
197+
defp defaults(nil, _acc, _path), do: %{}
198+
defp defaults([opts | rest], acc, path) when is_list(opts) do
199+
defaults([Enum.into(opts, %{}) | rest], acc, path)
200+
end
201+
defp defaults([%{name: name, embeds: embeds} | rest], acc, path) do
202+
acc = defaults(embeds, acc, [name | path])
203+
defaults(rest, acc, path)
204+
end
205+
defp defaults([%{name: name, default: value} | rest], acc, path) do
206+
funs = [name | path]
207+
|> Enum.reverse
208+
|> Enum.map(fn nested_name ->
209+
fn :get_and_update, data, next ->
210+
with {nil, inner_data} <- next.(data[nested_name] || %{}),
211+
data = Map.put(data, nested_name, inner_data),
212+
do: {nil, data}
213+
end
214+
end)
215+
216+
acc = put_in(acc, funs, value)
217+
defaults(rest, acc, path)
218+
end
219+
defp defaults([%{} | rest], acc, path) do
220+
defaults(rest, acc, path)
221+
end
222+
223+
defp changes(%Changeset{} = ch) do
224+
Enum.reduce(ch.changes, %{}, fn {k, v}, m ->
225+
case v do
226+
%Changeset{} -> Map.put(m, k, changes(v))
227+
x = [%Changeset{} | _] -> Map.put(m, k, Enum.map(x, &changes/1))
228+
_ -> Map.put(m, k, v)
194229
end
195230
end)
196231
end

test/params_test.exs

+58-6
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,11 @@ defmodule ParamsTest do
274274
result = Params.to_map(changeset)
275275

276276
assert result == %{
277-
foo: nil,
278277
bat: %{
279278
man: "BATMAN",
280279
wo: %{
281280
man: "BATWOMAN"
282-
},
283-
mo: nil
281+
}
284282
}
285283
}
286284
end
@@ -297,13 +295,67 @@ defmodule ParamsTest do
297295
result = Params.to_map(changeset)
298296

299297
assert result == %{
300-
foo: nil,
301298
bat: %{
302299
man: "Bruce",
303300
wo: %{
304301
man: "BATWOMAN"
305-
},
306-
mo: nil
302+
}
303+
}
304+
}
305+
end
306+
307+
defmodule DefaultNested do
308+
use Params.Schema, %{
309+
a: :string,
310+
b: :string,
311+
c: [field: :string, default: "C"],
312+
d: %{
313+
e: :string,
314+
f: :string,
315+
g: [field: :string, default: "G"],
316+
},
317+
h: %{
318+
i: :string,
319+
j: :string,
320+
k: [field: :string, default: "K"],
321+
},
322+
l: %{
323+
m: :string
324+
},
325+
n: %{
326+
o: %{
327+
p: [field: :string, default: "P"]
328+
}
329+
}
330+
331+
}
332+
end
333+
334+
test "to_map only returns submitted fields" do
335+
result = %{
336+
a: "A",
337+
d: %{
338+
e: "E",
339+
g: "g"
340+
}
341+
}
342+
|> DefaultNested.from
343+
|> Params.to_map
344+
345+
assert result == %{
346+
a: "A",
347+
c: "C",
348+
d: %{
349+
e: "E",
350+
g: "g"
351+
},
352+
h: %{
353+
k: "K"
354+
},
355+
n: %{
356+
o: %{
357+
p: "P"
358+
}
307359
}
308360
}
309361
end

0 commit comments

Comments
 (0)