Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: vic/params
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: c9fea01594
Choose a base ref
...
head repository: vic/params
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.0.1
Choose a head ref
  • 5 commits
  • 6 files changed
  • 3 contributors

Commits on Jun 21, 2016

  1. travis build status on master

    vic authored Jun 21, 2016
    Copy the full SHA
    e900dc9 View commit details

Commits on Jun 22, 2016

  1. 9 to map skip missing (#10)

    * Changed to_map behaviour for missing params, 9
    
    * README, 9
    lasseebert authored and vic committed Jun 22, 2016
    Copy the full SHA
    8c9367a View commit details
  2. Copy the full SHA
    3fa31a6 View commit details

Commits on Jul 11, 2016

  1. 11 nofile (#12)

    * Removed defparams, 11
    
    * Cleanup, 11
    
    * Got a working solution that defines modules in the right context, 11
    
    * Reimplemented defparams, 11
    lasseebert authored and vic committed Jul 11, 2016
    Copy the full SHA
    5b8f284 View commit details
  2. New release v2.0.1

    vic committed Jul 11, 2016
    Copy the full SHA
    b1f9dc4 View commit details
Showing with 197 additions and 77 deletions.
  1. +24 −5 README.md
  2. +51 −16 lib/params.ex
  3. +59 −48 lib/params/def.ex
  4. +4 −1 lib/params/schema.ex
  5. +1 −1 mix.exs
  6. +58 −6 test/params_test.exs
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ Easily define parameter structure and validate/cast with [Ecto.Schema][Ecto.Sche

```elixir
def deps do
[{:params, "~> 2.0.0"}]
[{:params, "~> 2.0.1"}]
end
```

@@ -192,10 +192,6 @@ required fields by ending them with a `!`, of course
the bang is removed from the field definition and is
only used to mark which fields are required by default.

The [Params.data](http://hexdocs.pm/params/Params.html#data/1)
and [Params.changes](http://hexdocs.pm/params/Params.html#changes/1) can be useful
for obtaining an struct or map from a changeset.

You can also create a module and define
your schema or custom changesets in it:

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

The [Params.data](http://hexdocs.pm/params/Params.html#data/1)
and [Params.to_map](http://hexdocs.pm/params/Params.html#to_map/1) can be useful
for obtaining a struct or map from a changeset.

Note that `Params.data` and `Params.to_map` have different behaviour: `data`
returns a struct which will include all valid params. `to_map` returns a map
that only includes the submitted keys and keys with default values:

```elixir
defmodule UserUpdateParams do
use Params.Schema, %{
name: :string,
age: :integer,
auditlog: [field: :boolean, default: true]
}
end

changeset = UserUpdateParams.from(%{name: "John"})

Params.data(changeset) # => %UserUpdateParams{name: "John", age: nil, auditlog: true}
Params.to_map(changeset) # => %{name: "John", auditlog: true}
```

## API Documentation

[API Documentation](https://hexdocs.pm/params/)
67 changes: 51 additions & 16 deletions lib/params.ex
Original file line number Diff line number Diff line change
@@ -33,22 +33,22 @@ defmodule Params do
@doc false
defmacro __using__([]) do
quote do
import Params.Def, only: [defparams: 1, defparams: 2]
import Params.Def, only: [defparams: 1, defparams: 2, defschema: 1]
end
end

@doc """
Transforms an Ecto.Changeset into a Map with atom keys.
Recursively traverses and transforms embedded changesets and skips the _id
primary key field.
Recursively traverses and transforms embedded changesets and skips keys that
was not part of params given to changeset
"""
@spec to_map(Changeset.t) :: map
def to_map(%Changeset{} = ch) do
ch
|> data
|> Map.from_struct
|> sanitize_map
def to_map(%Changeset{data: %{__struct__: module}} = ch) do
default = module |> schema |> defaults
change = changes(ch)

deep_merge(default, change)
end

@doc """
@@ -183,14 +183,49 @@ defmodule Params do
end)
end

defp sanitize_map(%{} = map) do
Enum.reduce(map, %{}, fn {k, v}, m ->
case {k, v} do
{:__meta__, _} -> m
{:_id, _} -> m
{k, %{__struct__: _} = struct} -> Map.put(m, k, struct |> Map.from_struct |> sanitize_map)
{k, %{} = nested} -> Map.put(m, k, sanitize_map(nested))
{k, v} -> Map.put(m, k, v)
defp deep_merge(%{} = map_1, %{} = map_2) do
Map.merge(map_1, map_2, &deep_merge_conflict/3)
end

defp deep_merge_conflict(_k, %{} = m1, %{} = m2) do
deep_merge(m1, m2)
end
defp deep_merge_conflict(_k, _v1, v2), do: v2

defp defaults(params, acc \\ %{}, path \\ [])
defp defaults([], acc, _path), do: acc
defp defaults(nil, _acc, _path), do: %{}
defp defaults([opts | rest], acc, path) when is_list(opts) do
defaults([Enum.into(opts, %{}) | rest], acc, path)
end
defp defaults([%{name: name, embeds: embeds} | rest], acc, path) do
acc = defaults(embeds, acc, [name | path])
defaults(rest, acc, path)
end
defp defaults([%{name: name, default: value} | rest], acc, path) do
funs = [name | path]
|> Enum.reverse
|> Enum.map(fn nested_name ->
fn :get_and_update, data, next ->
with {nil, inner_data} <- next.(data[nested_name] || %{}),
data = Map.put(data, nested_name, inner_data),
do: {nil, data}
end
end)

acc = put_in(acc, funs, value)
defaults(rest, acc, path)
end
defp defaults([%{} | rest], acc, path) do
defaults(rest, acc, path)
end

defp changes(%Changeset{} = ch) do
Enum.reduce(ch.changes, %{}, fn {k, v}, m ->
case v do
%Changeset{} -> Map.put(m, k, changes(v))
x = [%Changeset{} | _] -> Map.put(m, k, Enum.map(x, &changes/1))
_ -> Map.put(m, k, v)
end
end)
end
107 changes: 59 additions & 48 deletions lib/params/def.ex
Original file line number Diff line number Diff line change
@@ -1,79 +1,94 @@
defmodule Params.Def do

@moduledoc false

@doc false
defmacro defparams({name, _, [schema]}, [do: block]) do
block = Macro.escape(block)
quote bind_quoted: [name: name, schema: schema, block: block] do
Module.eval_quoted(__MODULE__, Params.Def.define(schema, name, block))
module_name = Params.Def.module_concat(Params, name)

defmodule module_name do
Params.Def.defschema(schema)
Code.eval_quoted(block, [], __ENV__)
end

Module.eval_quoted(__MODULE__, quote do
def unquote(name)(params, options \\ []) do
unquote(module_name).from(params, options)
end
end)
end
end

@doc false
defmacro defparams({name, _, [schema]}) do
quote bind_quoted: [name: name, schema: schema] do
Module.eval_quoted(__MODULE__, Params.Def.define(schema, name, nil))
module_name = Params.Def.module_concat(Params, name)

defmodule module_name do
Params.Def.defschema(schema)
end

Module.eval_quoted(__MODULE__, quote do
def unquote(name)(params) do
unquote(module_name).from(params)
end
end)
end
end

@doc false
def defschema(schema) do
defmacro defschema(schema) do
quote bind_quoted: [schema: schema] do
Module.eval_quoted(__MODULE__, Params.Def.define(schema, __MODULE__))
normalized_schema = Params.Def.normalize_schema(schema, __MODULE__)
Module.eval_quoted(__MODULE__, Params.Def.gen_root_schema(normalized_schema))

normalized_schema
|> Params.Def.build_nested_schemas
|> Enum.each(fn
{name, content} ->
Module.create(name, content, Macro.Env.location(__ENV__))
end)
end
end

@doc false
def define(schema, module) do
schema |> normalize_schema(module) |> gen_schema
end

@doc false
def define(schema, name, block) do
module = module_concat(Params, name)
[gen_schema(module, normalize_schema(schema, module), block), gen_from(module, name)]
end

defp gen_from(module, name) do
quote do
def unquote(name)(params, options \\ []) do
unquote(module).from(params, options)
end
def build_nested_schemas(schemas, acc \\ [])
def build_nested_schemas([], acc), do: acc
def build_nested_schemas([schema | rest], acc) do
embedded = Keyword.has_key?(schema, :embeds)
acc = if embedded do
sub_schema = Keyword.get(schema, :embeds)

module_def = {
sub_schema |> List.first |> Keyword.get(:module),
Params.Def.gen_root_schema(sub_schema)
}
new_acc = [module_def | acc]
build_nested_schemas(sub_schema, new_acc)
else
acc
end
build_nested_schemas(rest, acc)
end

def module_concat(parent, name) do
Module.concat [parent, Macro.camelize("#{name}")]
end

defp gen_schema(schema) do
def gen_root_schema(schema) do
quote do
unquote_splicing(embed_schemas(schema))
use Params.Schema

@schema unquote(schema)
@required unquote(field_names(schema, &is_required?/1))
@optional unquote(field_names(schema, &is_optional?/1))

schema do
unquote_splicing(schema_fields(schema))
end
end
end

defp gen_schema(:embeds, schema) do
module = schema |> List.first |> Keyword.get(:module)
gen_schema(module, schema, nil)
end

defp gen_schema(module, schema, block) do
quote do
defmodule unquote(module) do
unquote(gen_schema(schema))
unquote(block)
end
end
end

defp is_required?(field_schema) do
Keyword.get(field_schema, :required, false)
end
@@ -86,21 +101,17 @@ defmodule Params.Def do
schema |> Enum.filter_map(filter, &Keyword.get(&1, :name))
end

defp embed_schemas(schemas) do
embedded? = fn x -> Keyword.has_key?(x, :embeds) end
gen = fn x -> gen_schema(:embeds, Keyword.get(x, :embeds)) end
schemas |> Enum.filter_map(embedded?, gen)
end

defp schema_fields(schema) do
Enum.map(schema, &schema_field/1)
end

defp schema_field(meta) do
{call, name, type, opts} = {field_call(meta),
Keyword.get(meta, :name),
field_type(meta),
field_options(meta)}
{call, name, type, opts} = {
field_call(meta),
Keyword.get(meta, :name),
field_type(meta),
field_options(meta)
}
quote do
unquote(call)(unquote(name), unquote(type), unquote(opts))
end
@@ -127,7 +138,7 @@ defmodule Params.Def do
Keyword.drop(meta, [:module, :name, :field, :embeds, :required, :cardinality])
end

defp normalize_schema(dict, module) do
def normalize_schema(dict, module) do
Enum.reduce(dict, [], fn {k,v}, list ->
[normalize_field({module, k, v}) | list]
end)
5 changes: 4 additions & 1 deletion lib/params/schema.ex
Original file line number Diff line number Diff line change
@@ -44,7 +44,10 @@ defmodule Params.Schema do

@doc false
defmacro __using__(schema) do
Params.Def.defschema(schema)
quote bind_quoted: [schema: schema] do
import Params.Def, only: [defschema: 1]
Params.Def.defschema(schema)
end
end

@doc false
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ defmodule Params.Mixfile do

def project do
[app: :params,
version: "2.0.0",
version: "2.0.1",
elixir: "~> 1.2",
name: "Params",
source_url: github,
64 changes: 58 additions & 6 deletions test/params_test.exs
Original file line number Diff line number Diff line change
@@ -274,13 +274,11 @@ defmodule ParamsTest do
result = Params.to_map(changeset)

assert result == %{
foo: nil,
bat: %{
man: "BATMAN",
wo: %{
man: "BATWOMAN"
},
mo: nil
}
}
}
end
@@ -297,13 +295,67 @@ defmodule ParamsTest do
result = Params.to_map(changeset)

assert result == %{
foo: nil,
bat: %{
man: "Bruce",
wo: %{
man: "BATWOMAN"
},
mo: nil
}
}
}
end

defmodule DefaultNested do
use Params.Schema, %{
a: :string,
b: :string,
c: [field: :string, default: "C"],
d: %{
e: :string,
f: :string,
g: [field: :string, default: "G"],
},
h: %{
i: :string,
j: :string,
k: [field: :string, default: "K"],
},
l: %{
m: :string
},
n: %{
o: %{
p: [field: :string, default: "P"]
}
}

}
end

test "to_map only returns submitted fields" do
result = %{
a: "A",
d: %{
e: "E",
g: "g"
}
}
|> DefaultNested.from
|> Params.to_map

assert result == %{
a: "A",
c: "C",
d: %{
e: "E",
g: "g"
},
h: %{
k: "K"
},
n: %{
o: %{
p: "P"
}
}
}
end