Skip to content

Commit

Permalink
first impl
Browse files Browse the repository at this point in the history
  • Loading branch information
vic committed Jan 24, 2016
0 parents commit 9a0f95e
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
/_build
/cover
/deps
erl_crash.dump
*.ez
20 changes: 20 additions & 0 deletions README.md
@@ -0,0 +1,20 @@
# Params

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:

1. Add params to your list of dependencies in `mix.exs`:

def deps do
[{:params, "~> 0.0.1"}]
end

2. Ensure params is started before your application:

def application do
[applications: [:params]]
end

30 changes: 30 additions & 0 deletions config/config.exs
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :params, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:params, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
79 changes: 79 additions & 0 deletions lib/params.ex
@@ -0,0 +1,79 @@
defmodule Params do

@relations [:embed, :assoc]
alias Ecto.Changeset

def from(params, module, changeset_name \\ :changeset) do
changeset(module, change(module), params, changeset_name)
end

def changes(%Changeset{} = ch) do
Enum.reduce(ch.changes, %{}, fn {k, v}, m ->
case v do
%Changeset{} -> Map.put(m, k, changes(v))
_ -> Map.put(m, k, v)
end
end)
end

defp change(module) when is_atom(module) do
%{__struct__: module} |> Changeset.change
end

def required(module) when is_atom(module) do
module.__info__(:attributes)
|> Keyword.get(:required, ~w())
end

def optional(module) when is_atom(module) do
module.__info__(:attributes)
|> Keyword.get(:optional)
|> case do
nil ->
module.__changeset__ |> Map.keys
|> Enum.map(&Atom.to_string/1)
x -> x
end
end

def changeset(module, changeset, params, changeset_name)
when is_atom(module) and is_atom(changeset_name) do
{required, required_relations} =
relation_partition(module, required(module))

{optional, optional_relations} =
relation_partition(module, optional(module))

Changeset.cast(changeset, params, required, optional)
|> cast_relations(required_relations,
required: true, with: changeset_name)
|> cast_relations(optional_relations,
with: changeset_name)
end

defp relation_partition(module, names) do
types = module.__changeset__

names
|> Stream.map(fn x -> String.to_atom("#{x}") end)
|> Enum.reduce({[], []}, fn name, {fields, relations} ->
case Map.get(types, name) do
{type, _} when type in @relations ->
{fields, [{name, type} | relations]}
_ ->
{[Atom.to_string(name) | fields], relations}
end
end)
end

defp cast_relations(changeset, relations, opts) do
Enum.reduce(relations, changeset, fn
{name, type}, ch ->
case type do
:assoc -> Changeset.cast_assoc(ch, name, opts)
:embed -> Changeset.cast_embed(ch, name, opts)
end
end)
end

end
60 changes: 60 additions & 0 deletions lib/params/schema.ex
@@ -0,0 +1,60 @@
defmodule Params.Schema do

defp __use__(:ecto) do
quote do
require Ecto.Schema
import Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: true}
@timestamps_opts []
@foreign_key_type :binary_id
@before_compile Ecto.Schema

Module.register_attribute(__MODULE__, :ecto_fields, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_assocs, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_embeds, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_raw, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_autogenerate_insert, accumulate: true)
Module.register_attribute(__MODULE__, :ecto_autogenerate_update, accumulate: true)
Module.put_attribute(__MODULE__, :ecto_autogenerate_id, nil)
end
end

defp __use__(:params) do
quote do
Module.register_attribute(__MODULE__, :required, persist: true)
Module.register_attribute(__MODULE__, :optional, persist: true)

def from(params, changeset_name \\ :changeset) do
Params.from(params, __MODULE__, changeset_name)
end

def changes(params, changeset_name \\ :changeset) do
Params.changes(from(params, changeset_name))
end

def changeset(changeset, params) do
Params.changeset(__MODULE__, changeset, params, :changeset)
end
defoverridable [changeset: 2]

end
end

defmacro __using__(_opts) do
quote do
import Params.Schema, only: [schema: 1]
unquote(__use__(:ecto))
unquote(__use__(:params))
end
end

defmacro schema([do: definition]) do
quote do
Ecto.Schema.schema "params #{__MODULE__}" do
unquote(definition)
end
end
end

end
34 changes: 34 additions & 0 deletions mix.exs
@@ -0,0 +1,34 @@
defmodule Params.Mixfile do
use Mix.Project

def project do
[app: :params,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end

# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end

# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[{:ecto, "~> 1.1.1"},
{:mix_test_watch, "~> 0.2", only: :dev},
{:credo, "~> 0.2.5", only: :dev}]
end
end
7 changes: 7 additions & 0 deletions mix.lock
@@ -0,0 +1,7 @@
%{"bunt": {:hex, :bunt, "0.1.4"},
"credo": {:hex, :credo, "0.2.5"},
"decimal": {:hex, :decimal, "1.1.1"},
"ecto": {:hex, :ecto, "1.1.3"},
"fs": {:hex, :fs, "0.9.2"},
"mix_test_watch": {:hex, :mix_test_watch, "0.2.5"},
"poolboy": {:hex, :poolboy, "1.5.1"}}
101 changes: 101 additions & 0 deletions test/params_test.exs
@@ -0,0 +1,101 @@
defmodule ParamsTest do
use ExUnit.Case
doctest Params

alias Ecto.Changeset
import Ecto.Changeset

defmodule PetParams do
use Params.Schema
schema do
field :name
field :age, :integer
end
end

test "module has schema types" do
assert %{age: :integer,
name: :string,
id: :binary_id} ==
PetParams.__changeset__
end

test "defaults to no required fields" do
assert [] == Params.required PetParams
end

test "defaults to all optional fields" do
assert ~w(age id name) == Params.optional PetParams
end

test "from returns a changeset" do
ch = PetParams.from(%{})
assert %Changeset{} = ch
end

test "fields are castable" do
ch = PetParams.from(%{"age" => "2"})
assert 2 = Changeset.get_change(ch, :age)
end

defmodule LocationParams do
use Params.Schema
@required ~w(latitude longitude)
schema do
field :latitude, :float
field :longitude, :float
end
end

defmodule BusParams do
use Params.Schema
@required ~w(origin destination)
schema do
embeds_one :origin, LocationParams
embeds_one :destination, LocationParams
end
end

test "invalid changeset on missing params" do
assert %{valid?: false} = BusParams.from(%{})
end

test "only valid if nested required present" do
params = %{
"origin" => %{
"latitude" => 12.2,
"longitude" => 13.3
},
"destination" => %{
"latitude" => 12.2,
"longitude" => 13.3
}
}

assert %{valid?: true} = BusParams.from(params)
end

test "invalid if nested required missing" do
params = %{
"origin" => %{
"latitude" => 12.2,
},
"destination" => %{
"longitude" => 13.3
}
}

assert %{valid?: false} = BusParams.from(params)
end


test "changes gets casted values" do
params = %{
"origin" => %{
"latitude" => "12.2",
}
}
changes = BusParams.changes(params)
assert %{origin: %{latitude: 12.2}} = changes
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 9a0f95e

Please sign in to comment.