Created
April 12, 2018 11:29
-
-
Save rrrene/6aa14d5b2b3441c96a8e47b4ef12958e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Converter do | |
def convert_image(_a, _b, _c) do | |
:ok | |
end | |
end | |
defmodule Converter.StepBuilder do | |
defmacro __using__(_opts \\ []) do | |
quote do | |
Module.register_attribute(__MODULE__, :steps, accumulate: true) | |
@before_compile Converter.StepBuilder | |
import Converter.StepBuilder | |
def call(token) do | |
do_call(token) | |
end | |
end | |
end | |
defmacro step(do: clauses) do | |
Enum.reduce(clauses, nil, fn {:->, _, [[conditions], args]}, acc -> | |
# we collect all calls inside the current `->` block | |
quoted_calls = | |
case args do | |
{:__block__, _, quoted_calls} -> quoted_calls | |
single_quoted_call -> [single_quoted_call] | |
end | |
# and add conditions where applicable | |
quote do | |
unquote(acc) | |
unquote(add_conditions(quoted_calls, conditions)) | |
end | |
end) | |
end | |
defmacro step(module) do | |
quote do | |
@steps {unquote(module), true} | |
end | |
end | |
defmacro step(module, if: conditions) do | |
quote do | |
# the second element of the tuple stores the given condition | |
@steps {unquote(module), unquote(Macro.escape(conditions))} | |
end | |
end | |
defp add_conditions(list, conditions) when is_list(list) do | |
Enum.map(list, &add_conditions(&1, conditions)) | |
end | |
# quoted calls to our step/1 DSL macro look like this: | |
# | |
# {:step, _, [MyStepModule]} | |
# | |
# so all we have to do is append the `if:` condition | |
# | |
# {:step, _, [MyStepModule, [if: conditions]]} | |
# | |
defp add_conditions({:step, meta, args}, conditions) do | |
{:step, meta, args ++ [[if: conditions]]} | |
end | |
# if we encounter any other calls, we just leave them intact | |
defp add_conditions(ast, _conditions) do | |
ast | |
end | |
defmacro __before_compile__(env) do | |
steps = Module.get_attribute(env.module, :steps) | |
body = Converter.StepBuilder.compile(steps) | |
quote do | |
defp do_call(token) do | |
unquote(body) | |
end | |
end | |
|> inspect_formatted | |
end | |
def inspect_formatted(thing) do | |
thing | |
|> Macro.to_string() | |
|> Code.format_string!() | |
|> IO.puts() | |
thing | |
end | |
def compile(steps) do | |
token = quote do: token | |
# we use Enum.reduce/2 like before, but this time we are compiling all the | |
# calls at compile-time into one giant case-statement | |
Enum.reduce(steps, token, &compile_step/2) | |
end | |
defp compile_step({step, conditions}, acc) do | |
quoted_call = | |
quote do | |
unquote(step).call(token) | |
end | |
quote do | |
# instead of just calling the Step, we are compiling the given conditions | |
# into the call | |
result = unquote(compile_conditions(quoted_call, conditions)) | |
case result do | |
%Converter.Token{} = token -> | |
unquote(acc) | |
_ -> | |
raise unquote("expected #{inspect(step)}.call/1 to return a Token") | |
end | |
end | |
end | |
defp compile_conditions(quoted_call, true) do | |
# if no conditions were given, we simply call the Step | |
quoted_call | |
end | |
defp compile_conditions(quoted_call, conditions) do | |
quote do | |
# we have to use `var!/1` for our variable to be accessible | |
# by the code inside `conditions` | |
var!(token) = token | |
# to avoid "unused variable" warnings, we assign the variable to `_` | |
_ = var!(token) | |
if unquote(conditions) do | |
# if the given conditions are truthy, we call the Step | |
unquote(quoted_call) | |
else | |
# otherwise, we just return the token | |
token | |
end | |
end | |
end | |
end | |
defmodule Converter.Step do | |
@type t :: module | |
@callback call(token :: Converter.Step.t()) :: Converter.Step.t() | |
defmacro __using__(_opts \\ []) do | |
quote do | |
@behaviour Converter.Step | |
alias Converter.Token | |
end | |
end | |
end | |
defmodule Converter.MyProcess do | |
use Converter.StepBuilder | |
step Converter.Step.ParseOptions | |
step Converter.Step.ValidateOptions | |
# we'll provide the condition as a keyword | |
# step Converter.Step.PrepareConversion, if: token.errors == [] | |
# step Converter.Step.ConvertImages, if: token.errors == [] | |
# step Converter.Step.ReportResults, if: token.errors == [] | |
# please note that `if:` is not something Elixir provides, we will have to implement this ourselves | |
# also, we could have named this any way we want, `if:` just seemed obvious | |
# step Converter.Step.ReportErrors, if: token.errors != [] | |
step do | |
token.errors == [] -> | |
step Converter.Step.PrepareConversion | |
step Converter.Step.ConvertImages | |
step Converter.Step.ReportResults | |
token.errors != [] -> | |
step Converter.Step.ReportErrors | |
end | |
end | |
defmodule Converter.Token do | |
defstruct argv: nil, | |
glob: nil, | |
filenames: nil, | |
target_dir: nil, | |
format: nil, | |
errors: nil, | |
halted: nil, | |
results: nil | |
end | |
defmodule Mix.Tasks.ConvertImages do | |
use Mix.Task | |
alias Converter.MyProcess | |
alias Converter.Token | |
def run(argv) do | |
MyProcess.call(%Token{argv: argv}) | |
end | |
end | |
defmodule Converter.Step.ParseOptions do | |
use Converter.Step | |
@default_glob "./image_uploads/*" | |
@default_target_dir "./tmp" | |
@default_format "jpg" | |
def call(%Token{argv: argv} = token) do | |
{opts, args, _invalid} = | |
OptionParser.parse(argv, switches: [target_dir: :string, format: :string]) | |
glob = List.first(args) || @default_glob | |
target_dir = opts[:target_dir] || @default_target_dir | |
format = opts[:format] || @default_format | |
%Token{token | glob: glob, target_dir: target_dir, format: format} | |
end | |
end | |
defmodule Converter.Step.ValidateOptions do | |
use Converter.Step | |
def call(%Token{glob: glob, format: format} = token) do | |
filenames = Path.wildcard(glob) | |
errors = | |
[ | |
if(Enum.empty?(filenames), do: "No images found."), | |
if(!Enum.member?(~w[jpg png], format), do: "Unrecognized format: #{format}") | |
] | |
|> Enum.reject(&is_nil/1) | |
%Token{token | errors: errors} | |
end | |
end | |
defmodule Converter.Step.PrepareConversion do | |
use Converter.Step | |
def call(%Token{glob: glob, target_dir: target_dir} = token) do | |
File.mkdir_p!(target_dir) | |
filenames = Path.wildcard(glob) | |
%Token{token | filenames: filenames} | |
end | |
end | |
defmodule Converter.Step.ConvertImages do | |
use Converter.Step | |
def call(token) do | |
results = | |
Enum.map(token.filenames, fn filename -> | |
Converter.convert_image(filename, token.target_dir, token.format) | |
end) | |
%Token{token | results: results} | |
end | |
end | |
defmodule Converter.Step.ReportResults do | |
use Converter.Step | |
def call(token) do | |
IO.puts("Wrote #{Enum.count(token.results)} images to #{token.target_dir}.") | |
token | |
end | |
end | |
defmodule Converter.Step.ReportErrors do | |
use Converter.Step | |
def call(%Token{errors: errors} = token) do | |
Enum.each(errors, fn error -> | |
IO.puts("- #{error}") | |
end) | |
token | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment