From 97559eddcb00b9620dbc92bcd9bd454167e63272 Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Wed, 12 Sep 2018 19:29:35 -0400 Subject: [PATCH] Initial commit of lib Dummy ecto schema provided in macro. Plug+validation modules created by macro. Time to tackle the validation DSL --- .formatter.exs | 4 ++ .gitignore | 27 +++++++--- config/config.exs | 30 +++++++++++ lib/pharams.ex | 110 ++++++++++++++++++++++++++++++++++++++ lib/pharams_error_view.ex | 19 +++++++ mix.exs | 29 ++++++++++ test/pharams_test.exs | 8 +++ test/test_helper.exs | 1 + 8 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 .formatter.exs create mode 100644 config/config.exs create mode 100644 lib/pharams.ex create mode 100644 lib/pharams_error_view.ex create mode 100644 mix.exs create mode 100644 test/pharams_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore index 86e4c3f..25764a4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..14981c3 --- /dev/null +++ b/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 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" diff --git a/lib/pharams.ex b/lib/pharams.ex new file mode 100644 index 0000000..229f152 --- /dev/null +++ b/lib/pharams.ex @@ -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 diff --git a/lib/pharams_error_view.ex b/lib/pharams_error_view.ex new file mode 100644 index 0000000..f49c286 --- /dev/null +++ b/lib/pharams_error_view.ex @@ -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 diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..b125220 --- /dev/null +++ b/mix.exs @@ -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 diff --git a/test/pharams_test.exs b/test/pharams_test.exs new file mode 100644 index 0000000..fe88c23 --- /dev/null +++ b/test/pharams_test.exs @@ -0,0 +1,8 @@ +defmodule PharamsTest do + use ExUnit.Case + doctest Pharams + + test "greets the world" do + assert Pharams.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()