Skip to content

Commit

Permalink
Initial commit of lib
Browse files Browse the repository at this point in the history
Dummy ecto schema provided in macro. Plug+validation modules created by
macro. Time to tackle the validation DSL
  • Loading branch information
akoutmos committed Sep 12, 2018
1 parent 0d34cf7 commit 97559ed
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
27 changes: 21 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
/_build
/cover
/deps
/doc
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez
*.beam
/config/*.secret.exs

# Ignore package tarball (built via "mix hex.build").
pharams-*.tar

30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -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 your application as:
#
# config :pharams, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:pharams, :key)
#
# You can also 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"
110 changes: 110 additions & 0 deletions lib/pharams.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule Pharams do
@moduledoc """
Documentation for Pharams.
"""

def recusrive_map_from_struct(map) do
map
|> Map.from_struct()
|> Enum.map(fn
{key, %{__struct__: _} = value} -> {key, recusrive_map_from_struct(value)}
key_val -> key_val
end)
|> Map.new()
end

defmacro __using__(opts) do
error_module = Keyword.get(opts, :view_module, Pharams.ErrorView)
error_template = Keyword.get(opts, :view_template, "errors.json")
error_status = Keyword.get(opts, :error_status, :unprocessable_entity)

quote do
import Pharams, only: [pharams: 2]

def pharams_error_view_module, do: unquote(error_module)
def pharams_error_view_template, do: unquote(error_template)
def pharams_error_status, do: unquote(error_status)
end
end

defp generate_plug(validation_module, controller_module) do
quote do
use Phoenix.Controller

import Plug.Conn
import Ecto.Changeset

def init(opts), do: IO.inspect(opts)

def call(conn, key) do
validation_module = unquote(validation_module)
changeset = validation_module.changeset(struct(validation_module), conn.params)

if changeset.valid? do
new_params =
changeset
|> apply_changes()
|> Pharams.recusrive_map_from_struct()

%{conn | params: new_params}
else
controller_module = unquote(controller_module)

view_module = controller_module.pharams_error_view_module
view_template = controller_module.pharams_error_view_template
error_status = controller_module.pharams_error_status

conn
|> put_status(error_status)
|> render(view_module, view_template, changeset)
|> halt()
end
end
end
end

defp generate_validation do
quote do
use Ecto.Schema

import Ecto.Changeset

@primary_key false
embedded_schema do
field(:page, :integer)
field(:rawr, :integer)
end

def changeset(schema, params) do
schema
|> cast(params, [:page, :rawr])
|> validate_required([:page])
|> validate_number(:page, greater_than: 0)
end
end
end

defmacro pharams(controller_action, do: _block) do
camel_action =
controller_action
|> Atom.to_string()
|> Macro.camelize()

calling_module = __CALLER__.module

# Create validation module
validation_module_name = Module.concat([calling_module, PharamsValidator, camel_action])
validation_module_ast = generate_validation()
Module.create(validation_module_name, validation_module_ast, Macro.Env.location(__ENV__))

# Create Plug module
plug_module_name = Module.concat([calling_module, PharamsPlug, camel_action])
plug_module_ast = generate_plug(validation_module_name, calling_module)
Module.create(plug_module_name, plug_module_ast, Macro.Env.location(__ENV__))

# Insert the validation plug
quote do
plug(unquote(plug_module_name) when var!(action) == unquote(controller_action))
end
end
end
19 changes: 19 additions & 0 deletions lib/pharams_error_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Pharams.ErrorView do
@doc """
Traverses changeset errors.
"""

def translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end

def render("errors.json", %Ecto.Changeset{} = changeset) do
%{
errors: translate_errors(changeset)
}
end
end
29 changes: 29 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Pharams.MixProject do
use Mix.Project

def project do
[
app: :pharams,
version: "0.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:phoenix, "~> 1.3.4"},
{:plug, "~> 1.6.2"},
{:ecto, "2.2.10"}
]
end
end
8 changes: 8 additions & 0 deletions test/pharams_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule PharamsTest do
use ExUnit.Case
doctest Pharams

test "greets the world" do
assert Pharams.hello() == :world
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 97559ed

Please sign in to comment.