├── .gitignore ├── example ├── simple │ ├── README.md │ ├── lib │ │ ├── hello.ex │ │ ├── main.ex │ │ ├── app.ex │ │ └── server.ex │ ├── mix.exs │ └── mix.lock └── README.md ├── lib ├── match │ ├── path.ex │ ├── host.ex │ └── method.ex ├── interpreter │ ├── test.ex │ ├── printer.ex │ ├── conn.ex │ ├── transform.ex │ └── compiler.ex ├── plug │ ├── util.ex │ ├── send.ex │ ├── router.ex │ ├── export.ex │ └── import.ex ├── match.ex ├── derp.ex ├── pipeline.ex └── adapter │ └── cowboy.ex ├── .travis.yml ├── test └── spec │ ├── pipeline.spec.exs │ ├── spec_helper.exs │ ├── plug │ ├── export.spec.exs │ └── import.spec.exs │ └── adapter │ └── cowboy.spec.exs ├── mix.exs ├── mix.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | /doc 3 | /cover 4 | deps 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /example/simple/README.md: -------------------------------------------------------------------------------- 1 | # pipeline-example 2 | 3 | TODO: Document me! 4 | -------------------------------------------------------------------------------- /lib/match/path.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Match.Path do 2 | 3 | def normalize(path) do 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | Some examples using [pipeline]. 4 | 5 | [pipeline]: https://github.com/metalabdesign/pipeline 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.2.2 4 | otp_release: 5 | - 18.2.1 6 | env: 7 | - MIX_ENV=test 8 | script: mix coveralls.travis 9 | -------------------------------------------------------------------------------- /lib/interpreter/test.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Interpreter.Test do 2 | @moduledoc """ 3 | Test a pipeline. 4 | """ 5 | 6 | # TODO: Impl me! 7 | 8 | end 9 | -------------------------------------------------------------------------------- /lib/match/host.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Match.Host do 2 | 3 | 4 | @doc """ 5 | Convert kek. 6 | """ 7 | def normalize(host) do 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/spec/pipeline.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Pipeline do 2 | use ESpec 3 | 4 | import Pipeline 5 | 6 | describe "pipeline" do 7 | 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /test/spec/spec_helper.exs: -------------------------------------------------------------------------------- 1 | ESpec.configure fn(config) -> 2 | config.before fn -> 3 | {:shared, hello: :world} 4 | end 5 | 6 | config.finally fn(_shared) -> 7 | :ok 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/simple/lib/hello.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello do 2 | def init(_) do 3 | nil 4 | end 5 | def call(conn, _) do 6 | conn 7 | |> Plug.Conn.put_resp_content_type("text/plain") 8 | |> Plug.Conn.send_resp(200, "Hello world") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/interpreter/printer.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Interpreter.Printer do 2 | @moduledoc """ 3 | Print out a pipeline. 4 | """ 5 | 6 | def print(pipeline, prefix \\ "") do 7 | # run(pipeline, []) 8 | # |> Enum.map(fn x -> prefix <> "~> " <> x end) 9 | # |> Enum.join("\n") 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/plug/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Plug.Util do 2 | @doc """ 3 | Set the contents of the response body. Unfortunately Plug has no function 4 | to do this natively (you're always forced to set the status too) so we have 5 | our own here. 6 | """ 7 | def put_resp(conn, content) do 8 | %{conn | resp_body: content, state: :set} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/plug/send.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Plug.Send do 2 | def init(_) do 3 | nil 4 | end 5 | 6 | def call(%Plug.Conn{state: :unset}, _) do 7 | raise Plug.Conn.NotSentError 8 | end 9 | def call(%Plug.Conn{state: :set} = conn, _) do 10 | Plug.Conn.send_resp(conn) 11 | end 12 | def call(%Plug.Conn{} = conn, _) do 13 | conn 14 | end 15 | def call(other, _) do 16 | raise "Expected Plug.Conn but got: #{inspect other}" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/match/method.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Match.Method do 2 | 3 | 4 | 5 | @doc """ 6 | Converts a given method to its connection representation. 7 | 8 | The request method is stored in the `Plug.Conn` struct as an uppercase string 9 | (like `"GET"` or `"POST"`). This function converts `method` to that 10 | representation. 11 | 12 | ## Examples 13 | iex> Plug.Router.Utils.normalize_method(:get) 14 | "GET" 15 | """ 16 | def normalize(method) do 17 | method |> to_string |> String.upcase 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/match.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Match do 2 | 3 | def init(plug) do 4 | 5 | end 6 | 7 | def call(conn, opts) do 8 | # conn = plug.call(conn, plug_opts) 9 | # if (conn.private.Match) do 10 | # opts.consequent.call(conn, consequent_opts) 11 | # else 12 | # opts.alternate.call(conn, alternate_opts) 13 | # end 14 | conn 15 | end 16 | 17 | 18 | def inspect(options) do 19 | 20 | end 21 | 22 | 23 | 24 | def match(predicate) do 25 | 26 | end 27 | 28 | def host(conn, options) do 29 | 30 | end 31 | 32 | def path(path) do 33 | 34 | end 35 | 36 | 37 | end 38 | -------------------------------------------------------------------------------- /example/simple/lib/main.ex: -------------------------------------------------------------------------------- 1 | defmodule Entry do 2 | # Enable pipeline-related features and conveniences like the `~>` operator. 3 | use Pipeline 4 | 5 | # Your first pipeline! Coming from Plug, `pipeline` works as kind of a hybrid 6 | # version of `init/1` and `call/2`. Instead of configuring the options for 7 | # your connection handler as `init/1` does for Plug, you configure the entire 8 | # processing sequence. The result of this function is a pipeline, _NOT_ a 9 | # connection object. 10 | 11 | # @spec Pipeline.t(Plug.Conn) 12 | def pipeline do 13 | empty 14 | ~> plug(Hello) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example/simple/lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApplication do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start pipeline-based server. 11 | worker(Server, [[port: 4000]]), 12 | ] 13 | 14 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 15 | # for other strategies and supported options 16 | opts = [strategy: :one_for_one, name: MyApplication.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/simple/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PipelineExampleSimple.Mixfile do 2 | use Mix.Project 3 | 4 | def project do [ 5 | app: :pipeline_example_simple, 6 | version: "0.1.0", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps, 11 | ] end 12 | 13 | def application do [ 14 | applications: [:logger, :cowboy], 15 | mod: {MyApplication, []}, 16 | ] end 17 | 18 | defp deps do [ 19 | # Monadic effects. 20 | {:pipeline, path: "../../"}, 21 | # Plug things. 22 | {:plug, "1.1.2"}, 23 | # Cowboy server. 24 | {:cowboy, "~> 1.0.4"}, 25 | ] end 26 | end 27 | -------------------------------------------------------------------------------- /lib/derp.ex: -------------------------------------------------------------------------------- 1 | defmodule Derp do 2 | import Pipeline 3 | 4 | @doc """ 5 | Set the HTTP status code of the response. 6 | """ 7 | def status(value) when is_integer(value) do 8 | plug(&Plug.Conn.put_status/2, value) 9 | end 10 | 11 | @doc """ 12 | Set the response body. 13 | """ 14 | def body(content) do 15 | plug(&Pipeline.Util.put_resp/2, content) 16 | end 17 | 18 | @doc """ 19 | Set a response header. 20 | """ 21 | def put(header, value) do 22 | plug(&Plug.Conn.put_resp_header(&1, header, value)) 23 | end 24 | 25 | @doc """ 26 | Send the response. 27 | """ 28 | def send() do 29 | plug(&Plug.Conn.send_resp/1) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /example/simple/mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 2 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 3 | "effects": {:hex, :effects, "0.1.1", "1daab8e5bda97452910c7fb21ad167a406f71e428d0aad8acb99f775bf14fd0f", [:mix], []}, 4 | "plug": {:hex, :plug, "1.1.2", "7ed5cdc0245a56fcc9ad905c059dd2314cea9d043e6d0b8fd99b4845a3e220d5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]}, 5 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}} 6 | -------------------------------------------------------------------------------- /example/simple/lib/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Server do 2 | @moduledoc """ 3 | Using Pipeline's HTTP adapter instead of Plug's gives a few extra goodies. 4 | They're very similar under the hood, however. This setup will produce a 5 | worker process that you can feed into your application supervisor. 6 | """ 7 | 8 | # For `empty`, `~>`, and `plug`. 9 | import Pipeline 10 | 11 | # Even though we're _not_ using Plug's adapter, we're still using Plug's 12 | # connection object to pass around between stages. This ensures we can use 13 | # other plugs because that's the kind of object they will expect. If one 14 | # wanted to not rely on Plug at all, a different kind of connection object 15 | # could be provided. 16 | @connection Plug.Adapters.Cowboy.Conn 17 | 18 | # Generate all the necessary boilerplate. 19 | use Pipeline.Adapter.Cowboy, 20 | pipeline: empty 21 | ~> Entry.pipeline 22 | ~> plug(Pipeline.Plug.Send) 23 | end 24 | -------------------------------------------------------------------------------- /test/spec/plug/export.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Pipeline.Plug.Export do 2 | use ESpec 3 | use Plug.Test 4 | 5 | import Pipeline 6 | 7 | defmodule HelloPlug do 8 | def init(_) do 9 | nil 10 | end 11 | def call(conn, _) do 12 | conn 13 | |> Plug.Conn.put_resp_content_type("text/plain") 14 | |> Plug.Conn.send_resp(200, "Hello world.") 15 | end 16 | end 17 | 18 | defmodule SamplePlug do 19 | use Pipeline.Plug.Export 20 | def pipeline(options) do 21 | empty ~> plug(HelloPlug) 22 | end 23 | end 24 | 25 | describe "pipeline/plug/export" do 26 | it "should work" do 27 | # Create a fake connection object. 28 | conn = conn(:get, "/hello") 29 | # Invoke `SamplePlug` as a normal plug. 30 | conn = SamplePlug.call(conn, SamplePlug.init([])) 31 | # Ensure the correct response. 32 | expect conn.state |> to(eq :sent) 33 | expect conn.status |> to(eq 200) 34 | expect conn.resp_body |> to(eq "Hello world.") 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/spec/adapter/cowboy.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Pipeline.Adapter.Cowboy do 2 | use ESpec 3 | use Plug.Test 4 | 5 | import Pipeline 6 | 7 | defmodule HelloPlug do 8 | def init(_) do 9 | nil 10 | end 11 | def call(conn, _) do 12 | conn 13 | |> Plug.Conn.put_resp_content_type("text/plain") 14 | |> Plug.Conn.send_resp(200, "Hello world.") 15 | end 16 | end 17 | 18 | defmodule SamplePipeline do 19 | def pipeline() do 20 | empty ~> plug(HelloPlug) 21 | end 22 | end 23 | 24 | defmodule SampleServer do 25 | @connection Plug.Adapters.Cowboy.Conn 26 | use Pipeline.Adapter.Cowboy, pipeline: SamplePipeline.pipeline 27 | end 28 | 29 | describe "pipeline/adapter/cowboy" do 30 | it "should work" do 31 | {:ok, pid} = SampleServer.start_link [port: 4000] 32 | %{ 33 | status_code: status, 34 | body: body, 35 | } = HTTPoison.get! "http://localhost:4000/" 36 | Process.exit(pid, :normal) 37 | expect status |> to(eq 200) 38 | expect body |> to(eq "Hello world.") 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /test/spec/plug/import.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Pipeline.Plug.Import do 2 | use ESpec 3 | use Plug.Test 4 | 5 | import Pipeline 6 | 7 | defmodule HelloPlug do 8 | def init(_) do 9 | nil 10 | end 11 | def call(conn, _) do 12 | conn 13 | |> Plug.Conn.put_resp_content_type("text/plain") 14 | |> Plug.Conn.send_resp(200, "Hello world.") 15 | end 16 | end 17 | 18 | defmodule SamplePipeline do 19 | def pipeline(options) do 20 | empty ~> plug(HelloPlug) 21 | end 22 | end 23 | 24 | defmodule PipelineConsumer do 25 | # Needed for being able to call `plug some_pipeline`. 26 | use Pipeline.Plug.Import 27 | # Needed for using the `plug` macro. 28 | use Plug.Builder 29 | 30 | # Access a pipeline as if it were a plug! 31 | Plug.Builder.plug SamplePipeline 32 | end 33 | 34 | describe "pipeline/plug/import" do 35 | it "should work" do 36 | # Create a fake connection object. 37 | conn = conn(:get, "/hello") 38 | # Invoke `PipelineConsumer` as a normal plug. 39 | conn = PipelineConsumer.call(conn, PipelineConsumer.init([])) 40 | # Ensure the correct response. 41 | expect conn.state |> to(eq :sent) 42 | expect conn.status |> to(eq 200) 43 | expect conn.resp_body |> to(eq "Hello world.") 44 | end 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/plug/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Plug.Router do 2 | @moduledoc """ 3 | Just like Plug's Router! 4 | 5 | Generates pipeline/0 and pipeline/1 for your module. 6 | """ 7 | 8 | defmacro __using__(opts) do 9 | quote do 10 | # Collect list of plugs. 11 | Module.register_attribute(__MODULE__, :plugs, accumulate: true) 12 | # Convert plugs into pipeline/0. 13 | @before_compile unquote(__MODULE__) 14 | 15 | @doc """ 16 | Provides equivalent to Plug's plug macro that can be used in module 17 | definitions. 18 | """ 19 | defmacro plug(name, opts \\ []) do 20 | quote do 21 | @plugs {unquote(name), unquote(opts)} 22 | end 23 | end 24 | end 25 | end 26 | 27 | @doc """ 28 | 29 | """ 30 | defmacro __before_compile__(env) do 31 | parts = Keyword.get(env.module, :plugs) 32 | base = quote do: Pipeline.empty 33 | code = parts |> Enum.reduce(base, fn ({name, opts}, prev) -> 34 | quote do 35 | unquote(prev) ~> Pipeline.plug(unquote(name), unquote(opts)) 36 | end 37 | end) 38 | quote do 39 | # Generate pipeline/0. 40 | def pipeline do 41 | unquote(code) 42 | end 43 | 44 | # Generate pipeline/1 as an alias to pipeline/0. Useful for consumption 45 | # as a plug because plugs are always allowed to pass options and when 46 | # using this DSL the options are totally irrelevent. 47 | def pipeline(_) do 48 | pipeline 49 | end 50 | end 51 | end 52 | 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/interpreter/conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Interpreter.Conn do 2 | @moduledoc """ 3 | Run a pipeline with a given connection. This is the runtime equivalent of the 4 | pipeline compiler. It's still performant, but compiled code is always faster. 5 | The interpreter has one advantage in that you can use monadic bind to change 6 | the pipeline based on the value of the connection which is otherwise not 7 | possible in compilation, because the connection is not available. 8 | """ 9 | 10 | import Effects, only: [queue_apply: 2] 11 | alias Effects.Pure 12 | alias Effects.Effect 13 | alias Pipeline.Effects 14 | 15 | @doc """ 16 | Derp. 17 | """ 18 | defp effect({_, conn}, %Pure{value: value}) do 19 | {value, conn} 20 | end 21 | 22 | @doc """ 23 | Derp. 24 | """ 25 | defp effect({value, conn}, %Effect{ 26 | effect: %Effects.Halt{} = entry, 27 | next: next, 28 | }) do 29 | {entry, conn} 30 | end 31 | 32 | @doc """ 33 | Herp. 34 | """ 35 | defp effect({value, conn}, %Effect{ 36 | effect: %Effects.Match{patterns: patterns} = entry, 37 | next: next, 38 | }) do 39 | {_, pipeline} = patterns |> Enum.find(fn {guard, _} -> 40 | nil # TODO! 41 | end) 42 | nil |> effect(queue_apply(next, entry)) 43 | end 44 | 45 | @doc """ 46 | kek. 47 | """ 48 | defp effect({value, conn}, %Effect{ 49 | effect: %Effects.Plug{plug: plug, options: options} = entry, 50 | next: next, 51 | }) do 52 | case apply(plug, :call, [conn, plug.init(options)]) do 53 | # TODO Should this case be explicity checked for? It's fairly specific 54 | # to plug. Should we check the result of this function returns plugs? 55 | %{halted: true} = conn -> {value, conn} 56 | _ = conn -> {value, conn} |> effect(queue_apply(next, entry)) 57 | end 58 | end 59 | 60 | def run(init, pipeline) do 61 | {_, conn} = {nil, init} |> effect(pipeline) 62 | conn 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Mixfile do 2 | use Mix.Project 3 | 4 | def project do [ 5 | app: :pipeline, 6 | version: "0.1.0", 7 | description: description, 8 | package: package, 9 | elixir: "~> 1.2", 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps, 13 | spec_pattern: "*.spec.exs", 14 | aliases: aliases, 15 | spec_paths: [ 16 | "test/spec", 17 | ], 18 | test_coverage: [ 19 | tool: ExCoveralls, 20 | test_task: "espec", 21 | ], 22 | preferred_cli_env: [ 23 | "espec": :test, 24 | "coveralls": :test, 25 | "coveralls.detail": :test, 26 | "coveralls.post": :test, 27 | ], 28 | ] end 29 | 30 | def application do [ 31 | applications: applications(Mix.env), 32 | ] end 33 | 34 | defp applications(:test) do [ 35 | :cowboy, 36 | :httpoison, 37 | ] end 38 | 39 | defp applications(:dev) do [ 40 | :cowboy, 41 | :httpoison, 42 | ] end 43 | 44 | defp description do 45 | """ 46 | Monadic HTTP application composition for plug and friends. 47 | """ 48 | end 49 | 50 | defp package do [ 51 | name: :pipeline, 52 | files: ["lib", "mix.exs", "README*"], 53 | maintainers: ["Izaak Schroeder"], 54 | licenses: ["CC0-1.0"], 55 | links: %{"GitHub" => "https://github.com/metalabdesign/pipeline"} 56 | ] end 57 | 58 | defp aliases do [ 59 | lint: ["dogma"], 60 | test: ["coveralls"], 61 | ] end 62 | 63 | defp deps do [ 64 | # Monadic effects. 65 | {:effects, "~> 0.1.1"}, 66 | # Support for plug. 67 | {:plug, "~> 1.1.2", optional: true}, 68 | # Test coverage. 69 | {:excoveralls, "~> 0.4", only: [:dev, :test]}, 70 | # Static analysis. 71 | {:dialyxir, "~> 0.3", only: [:dev, :test]}, 72 | # Test-style. 73 | {:espec, "~> 0.8.17", only: [:dev, :test]}, 74 | # Linting. 75 | {:dogma, "~> 0.1.4", only: [:dev, :test]}, 76 | # Documentation generation. 77 | {:ex_doc, "~> 0.13.0", only: [:dev]}, 78 | # Cowboy HTTP server. 79 | {:cowboy, "~> 1.0.4", only: [:dev, :test]}, 80 | # HTTP requests during testing. 81 | {:httpoison, "~> 0.9.0", only: [:dev, :test]}, 82 | ] end 83 | end 84 | -------------------------------------------------------------------------------- /lib/plug/export.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Plug.Export do 2 | @moduledoc """ 3 | TODO: Write me. 4 | """ 5 | 6 | # Use the pipeline connection interpreter. 7 | alias Pipeline.Interpreter.Conn 8 | 9 | # Anyone that has our behaviour must implement `pipeline/1` as a function that 10 | # generates a pipeline from some given options. 11 | @callback pipeline(any) :: any 12 | 13 | defmacro __using__(opts) do 14 | quote do 15 | # Pipelines are also plugs! Implementing this behaviour means the module 16 | # must have init/1 and call/2, both of which are provided by this macro. 17 | @behaviour Plug 18 | 19 | # In order to generate `init/1` and `call/2` we need access to the 20 | # generated pipeline, and thus some function must _generate_ that 21 | # pipeline. We define `pipeline/1` as the function that does this, with 22 | # its first argument being the argument to `init/1` (that is to say the 23 | # configuration options for the plug interface define the configuration 24 | # options for the pipeline). 25 | @behaviour Pipeline.Plug.Export 26 | 27 | @doc """ 28 | Setup the pipeline based on the given configuration options. The result 29 | of this is then saved by Plug in your application and fed to `call/2` as 30 | the second argument. 31 | """ 32 | def init(options) do 33 | options 34 | end 35 | 36 | @doc """ 37 | Run the connection through the configured pipeline. Note that this is 38 | not nearly as performant as a compiled version, but it's the way it has 39 | to be for plug. Plug's `init` has to return a representation that is 40 | serializable (no anonymous functions) because its results get passed to 41 | `Macro.escape` eventually. If Plug were to call `init` when the program 42 | started (like via module_loaded hook) all of this would be solved. Alas, 43 | here we are. 44 | 45 | Note that using Pipeline this way is not necessary; you can use Pipeline's 46 | interop pre-compilation hook and compile your pipeline the same way Plug 47 | works, but you need to `use Pipeline.Plug.Import` in your plug-based 48 | module. 49 | """ 50 | def call(conn, options) do 51 | conn |> Conn.run(pipeline(options)) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/interpreter/transform.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Interpreter.Transform do 2 | @moduledoc """ 3 | Alter pipelines. Basically just a free interpreter whose state is also a 4 | pipeline being built up using the transformed existing pipeline. 5 | """ 6 | 7 | import Pipeline 8 | 9 | defmodule Identity do 10 | @moduledoc """ 11 | A transform that just returns the existing pipeline. This is mainly useful 12 | in that other transforms can extend this one and then only need to add 13 | effect handlers for the things they are interested in. 14 | """ 15 | 16 | @doc """ 17 | 18 | """ 19 | def effect(pipeline, %Effects.Effect{ 20 | effect: effect, 21 | next: next, 22 | }) do 23 | nil |> effect(next.(nil)) 24 | end 25 | 26 | @doc """ 27 | 28 | """ 29 | def effect(pipeline, %Effects.Pure{ 30 | value: value, 31 | }) do 32 | nil 33 | end 34 | end 35 | 36 | defmodule DebugPipeline do 37 | @moduledoc """ 38 | Insert debug logging statements between all parts of a pipeline. 39 | """ 40 | 41 | def log(%Pipeline.Effects.Plug{plug: plug, options: options}) do 42 | 43 | end 44 | 45 | def effect(pipeline, %Effects.Effect{ 46 | effect: effect, 47 | next: next, 48 | }) do 49 | # pipeline ~> log(effect) ~> effect(next.()) 50 | end 51 | end 52 | 53 | defmodule PlugToPipeline do 54 | @moduledoc """ 55 | Turn `plug(SomePlug, options)` into `SomePlug.pipeline(options)` for plugs 56 | which are pipelines. This is essentially an optimization pass resulting in less overhead. 57 | """ 58 | def effect(pipeline, %Effects.Effect{ 59 | effect: %Pipeline.Effects.Plug{plug: plug, options: options} = entry, 60 | next: next, 61 | }) do 62 | case function_exported?(plug, :pipeline, 1) do 63 | true -> pipeline ~> apply(plug, :pipeline, [options]) ~>> next 64 | _ -> pipeline ~> entry ~>> next 65 | end 66 | end 67 | end 68 | 69 | defmodule MatchFolding do 70 | @moduledoc """ 71 | Turn match([x ~> halt]) ~> match([y ~> halt]) into 72 | match([x ~> halt, y ~> halt]) 73 | """ 74 | end 75 | 76 | defmodule ActionFusion do 77 | @moduledoc """ 78 | Fuse together several component actions in sequence to a single function. 79 | e.g. Convert `status(200) ~> body("Hello World.") ~> send()` into a single 80 | `plug(fn conn -> conn |> send_resp(200, "Hello World") end)`. 81 | 82 | """ 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/interpreter/compiler.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Interpreter.Compiler do 2 | @moduledoc """ 3 | Convert a pipeline into an elixir AST. 4 | 5 | Because the plug chain is stored as essentially an abstract syntax tree we 6 | can perform any number of optimizations on it. 7 | """ 8 | 9 | import Effects, only: [queue_apply: 2] 10 | alias Effects.Pure 11 | alias Effects.Effect 12 | alias Pipeline.Effects 13 | 14 | defp convert(target, plug, options) when is_atom(plug) do 15 | case Atom.to_char_list(plug) do 16 | 'Elixir.' ++ _ -> quote do 17 | unquote(plug).call( 18 | unquote(target), 19 | unquote(Macro.escape(plug.init(options))) 20 | ) 21 | end 22 | _ -> quote do 23 | unquote(plug)(unquote(target), unquote(Macro.escape(options))) 24 | end 25 | end 26 | end 27 | 28 | defp convert(target, plug, options) when is_function(plug) do 29 | raise "TODO: Make this work." 30 | end 31 | 32 | @doc """ 33 | Derp. 34 | """ 35 | defp effect({_, compilation}, %Pure{value: value}) do 36 | {value, compilation} 37 | end 38 | 39 | @doc """ 40 | Derp. 41 | """ 42 | defp effect({value, compilation}, %Effect{ 43 | effect: %Effects.Halt{} = entry, 44 | }) do 45 | {entry, compilation} 46 | end 47 | 48 | @doc """ 49 | Herp. 50 | """ 51 | defp effect(state, %Effect{ 52 | effect: %Effects.Match{patterns: patterns} = entry, 53 | next: next, 54 | }) do 55 | {value, compilation} = state |> effect(queue_apply(next, entry)) 56 | {entry, {:cond, [], [[do: patterns |> Enum.map(fn {guard, pipeline} -> 57 | {:->, [], [[true], compilation |> compile(pipeline)]} 58 | end)]]}} 59 | end 60 | 61 | @doc """ 62 | wop. 63 | """ 64 | defp effect(state, %Effect{ 65 | effect: %Effects.Error{handler: handler} = entry, 66 | next: next, 67 | }) do 68 | foo = quote do: x 69 | {value, compilation} = state 70 | {_, bar} = {entry, foo} |> compile(handler) 71 | {value, quote do 72 | try do 73 | unquote(compilation) 74 | catch 75 | unquote(foo) -> unquote(bar) 76 | end 77 | end} |> effect(queue_apply(next, entry)) 78 | end 79 | 80 | @doc """ 81 | kek. 82 | """ 83 | defp effect(state, %Effect{ 84 | effect: %Effects.Plug{plug: plug, options: options} = entry, 85 | next: next, 86 | }) do 87 | {value, target} = state 88 | foo = quote do: x 89 | {_, compilation} = {value, foo} |> effect(queue_apply(next, entry)) 90 | {entry, quote do 91 | case unquote(convert(target, plug, options)) do 92 | %Plug.Conn{halted: true} = x -> x 93 | %Plug.Conn{} = unquote(foo) -> unquote(compilation) 94 | _ -> raise unquote("Must return a plug connection.") 95 | end 96 | end} 97 | end 98 | 99 | defp effect(_, effect) do 100 | raise "Expected valid pipeline effect object, but got #{inspect effect}." 101 | end 102 | 103 | @doc """ 104 | 105 | """ 106 | def compile(init, pipeline) do 107 | {_, compilation} = {nil, init} |> effect(pipeline) 108 | compilation 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/plug/import.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Plug.Import do 2 | @moduledoc """ 3 | This module lets you take advantage of Plug's static compilation mechanism 4 | from within Pipeline. When you have a module that invokes `plug Pipeline` 5 | this pre-compilation hook generates a private function within that module 6 | representing the invocation of that pipeline and then replaces the plug call 7 | with the generated private function. 8 | 9 | Unfortunately there is currently no automatic mechanism for doing this, so 10 | you're stuck with having to `use` this module to gain its beneficial effects. 11 | 12 | **IMPORTANT**: You must `use` this module _before_ you `use` anything from 13 | Plug, since it modifies the internal list of plugs. 14 | 15 | ```elixir 16 | defmodule SamplePipeline do 17 | def pipeline(options) do 18 | empty ~> Pipeline.plug(...) 19 | end 20 | end 21 | 22 | defmodule PipelineConsumer do 23 | # Needed for being able to call `plug some_pipeline`. This MUST come before 24 | # any plug-related `use` calls. 25 | use Pipeline.Plug.Import 26 | # Needed for using the `plug` macro. 27 | use Plug.Builder 28 | 29 | # Access a pipeline as if it were a plug! 30 | Plug.Builder.plug SamplePipeline 31 | end 32 | ``` 33 | """ 34 | 35 | alias Pipeline.Interpreter.Compiler 36 | 37 | defmacro __using__(_) do 38 | quote do 39 | @before_compile unquote(__MODULE__) 40 | end 41 | end 42 | 43 | defmacro __before_compile__(env) do 44 | # Get the list of existing plugs. This attribute is previously registered 45 | # by `Plug.Builder` and appended to via the `plug` macro. 46 | plugs = Module.get_attribute(env.module, :plugs) 47 | 48 | # The variable used here has to be passed around in the same module 49 | # everywhere – there can't be a `conn` defined here and another defined in 50 | # the compiler because they have different contexts. 51 | conn = quote do: conn 52 | 53 | # Find all plugs that are pipelines and compile those pipelines. Results 54 | # in a new list of plugs that contain the quoted compiled pipeline for 55 | # those entries which are pipelines. 56 | updates = plugs 57 | |> Enum.with_index() 58 | |> Enum.map(fn {{plug, options, _} = entry, index} -> 59 | case Keyword.get(plug.__info__(:functions), :pipeline) do 60 | 1 -> 61 | # Generate a unique name for the internal function. 62 | # TODO: Consider a more bullet-proof mechanism than this? 63 | uniq = String.to_atom("__pipeline_int_" <> Integer.to_string(index)) 64 | { 65 | {uniq, nil, true}, 66 | quote do 67 | defp unquote(uniq)(conn, _) do 68 | unquote( 69 | conn |> Compiler.compile(plug.pipeline(options)) 70 | ) 71 | end 72 | end, 73 | } 74 | _ -> {entry, nil} 75 | end 76 | end) 77 | 78 | # Rewrite the list of plugs with our new generated function. 79 | Module.delete_attribute(env.module, :plugs) 80 | updates 81 | |> Enum.map(fn {plug, _} -> plug end) 82 | |> Enum.each(&Module.put_attribute(env.module, :plugs, &1)) 83 | 84 | # Inject the generated functions into the consumer module. 85 | updates 86 | |> Enum.map(fn {_, q} -> q end) 87 | |> Enum.filter(fn x -> x end) 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 3 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 4 | "dialyxir": {:hex, :dialyxir, "0.3.5", "eaba092549e044c76f83165978979f60110dc58dd5b92fd952bf2312f64e9b14", [:mix], []}, 5 | "dogma": {:hex, :dogma, "0.1.7", "927f76a89a809db96e0983b922fc899f601352690aefa123529b8aa0c45123b2", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, optional: false]}]}, 6 | "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, 7 | "effects": {:hex, :effects, "0.1.1", "1daab8e5bda97452910c7fb21ad167a406f71e428d0aad8acb99f775bf14fd0f", [:mix], []}, 8 | "espec": {:hex, :espec, "0.8.28", "f002710673d215876c4ca6fc74cbf5e330954badea7389d2284d2050940f1779", [:mix], [{:meck, "~> 0.8.4", [hex: :meck, optional: false]}]}, 9 | "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 10 | "excoveralls": {:hex, :excoveralls, "0.5.5", "d97b6fc7aa59c5f04f2fa7ec40fc0b7555ceea2a5f7e7c442aad98ddd7f79002", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, 11 | "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, 12 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]}, 13 | "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 14 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 15 | "jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []}, 16 | "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 18 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 19 | "plug": {:hex, :plug, "1.1.6", "8927e4028433fcb859e000b9389ee9c37c80eb28378eeeea31b0273350bf668b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]}, 20 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 21 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}, 22 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} 23 | -------------------------------------------------------------------------------- /lib/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline do 2 | @moduledoc """ 3 | Pipeline is a monad-based alternative to Plug's builder and router. It offers 4 | much of the same functionality. 5 | 6 | This module can be `use`-d into a module in order to build a pipeline: 7 | 8 | ```elixir 9 | defmodule MyApp do 10 | use Pipeline 11 | 12 | def hello(conn, opts) do 13 | Plug.Conn.send_resp(conn, 200, body) 14 | end 15 | 16 | def pipeline(options) do 17 | empty 18 | ~> plug(Plug.Logger) 19 | ~> plug(:hello) 20 | end 21 | end 22 | ``` 23 | """ 24 | 25 | use Effects 26 | 27 | # ---------------------------------------------------------- 28 | # Actions 29 | # ---------------------------------------------------------- 30 | defmodule Effects do 31 | defmodule Plug do 32 | @type t :: %Plug{plug: atom, options: term} 33 | defstruct [:plug, :options] 34 | end 35 | 36 | defmodule Match do 37 | @type t :: %Match{patterns: term} 38 | defstruct [:patterns] 39 | end 40 | 41 | defmodule Error do 42 | @type t :: %Error{handler: term} 43 | defstruct [:handler] 44 | end 45 | 46 | defmodule Halt do 47 | @type t :: %Halt{} 48 | defstruct [] 49 | end 50 | end 51 | 52 | # Our effects are not really extensible since open-union types are not 53 | # possible here :( 54 | # @type e :: Effects.Plug.t | Effects.Match.t | Effects.Halt.t 55 | # @type t :: Free.t(e) 56 | 57 | # ---------------------------------------------------------- 58 | # Effect Creators 59 | # ---------------------------------------------------------- 60 | defeffect plug(_plug, _options, _guards) do 61 | raise ArgumentError, message: "Plug guards not supported by Pipeline." 62 | end 63 | 64 | @doc """ 65 | Use a plug. 66 | """ 67 | defeffect plug(plug, options \\ nil) do 68 | %Effects.Plug{plug: plug, options: options} 69 | end 70 | 71 | @doc """ 72 | Basically a structural `cond` or `case` statement. 73 | """ 74 | defeffect match(patterns) do 75 | %Effects.Match{patterns: patterns} 76 | end 77 | 78 | @doc """ 79 | Handle some errors. 80 | """ 81 | defeffect error(handler) do 82 | %Effects.Error{handler: handler} 83 | end 84 | 85 | @doc """ 86 | Halt the pipeline. 87 | """ 88 | defeffect halt() do 89 | %Effects.Halt{} 90 | end 91 | 92 | def match(predicate, consequent) do 93 | match([{predicate, consequent}]) 94 | end 95 | 96 | def match(predicate, consequent, alternate) do 97 | match([{predicate, consequent}, {true, alternate}]) 98 | end 99 | 100 | @doc """ 101 | The "unit". Mainly just useful for chaining off of so things don't look weird. 102 | You can do things like: `empty ~> foo ~> bar`. 103 | """ 104 | def empty() do 105 | pure(nil) 106 | end 107 | 108 | @doc """ 109 | The ~>> operator. 110 | """ 111 | defdelegate a ~>> b, to: Elixir.Effects 112 | 113 | @doc """ 114 | The ~> operator. 115 | """ 116 | defdelegate a ~> b, to: Elixir.Effects 117 | 118 | @doc """ 119 | 120 | """ 121 | defdelegate fmap(a, b), to: Elixir.Effects 122 | 123 | @doc """ 124 | 125 | """ 126 | defdelegate ap(a, b), to: Elixir.Effects 127 | 128 | # ---------------------------------------------------------- 129 | # Other 130 | # ---------------------------------------------------------- 131 | defmacro __using__(opts) do 132 | quote do 133 | # Automatic shorthand for the various pipeline creators. 134 | import Pipeline 135 | 136 | # Quick access to match predicates. 137 | alias Pipeline.Match 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/adapter/cowboy.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline.Adapter.Cowboy do 2 | @moduledoc """ 3 | Pretty much built off Plug's adapter, but simpler and supports the static 4 | optimization mechanisms provided by pipeline. 5 | 6 | Invoking `use Pipeline.Adapter.Cowboy` will create a pipeline/1 function that 7 | accepts cowboy connects to be run through the pipeline specified by the 8 | pipeline parameter. This module should be used in your server process and 9 | needs little more than that to be setup. 10 | 11 | Your server, could for example, look like: 12 | 13 | ```elixir 14 | defmodule Server do 15 | use Pipeline.Adapter.Cowboy, pipeline: Entry.pipeline 16 | end 17 | ``` 18 | 19 | Your supervisor should add your server as a worker: 20 | 21 | ```elixir 22 | children = [ 23 | # Start pipeline-based server. 24 | worker(Server, [[port: 4000]]), 25 | ] 26 | 27 | You can configure your cowboy application with a number of other attributes. 28 | 29 | * acceptors 30 | * protocol 31 | * port 32 | 33 | Note that you do NOT need to use Plug connection objects to use this adapter, 34 | though using any plug-based pipeline's will require it. 35 | ``` 36 | 37 | TODO: Add more options to be at feature-parity with Plug's adapter. 38 | """ 39 | 40 | alias Pipeline.Interpreter.Compiler 41 | 42 | @doc """ 43 | Make sure cowboy is running. 44 | """ 45 | def ensure_cowboy do 46 | case Application.ensure_all_started(:cowboy) do 47 | {:ok, _} -> 48 | :ok 49 | {:error, {:cowboy, _}} -> 50 | raise "could not start the cowboy application. Please ensure it is" <> 51 | "listed as a dependency both in deps and application in your mix.exs" 52 | end 53 | end 54 | 55 | defmacro __before_compile__(env) do 56 | pipeline = Module.get_attribute(env.module, :pipeline) 57 | # The variable used here has to be passed around in the same module 58 | # everywhere – there can't be a `conn` defined here and another defined in 59 | # the compiler because they have different contexts. 60 | conn = quote do: conn 61 | # Generate the compiled version of the application pipeline. 62 | body = conn |> Compiler.compile(pipeline) 63 | # Inject it into the module as pipeline/1. 64 | quote do: def pipeline(unquote(conn)), do: unquote(body) 65 | end 66 | 67 | defmacro __using__(opts) do 68 | quote do 69 | # Default cowboy reference name. 70 | @ref Module.concat(__MODULE__, "_pipeline") 71 | 72 | # Default number of cowboy acceptors. 73 | @acceptors 100 74 | 75 | # ??? Copied from plug lol. 76 | @already_sent {:plug_conn, :sent} 77 | 78 | # Default pipeline to use for processing connections. 79 | @pipeline unquote(Keyword.get(opts, :pipeline)) 80 | 81 | # Default listening protocol. 82 | @protocol :http 83 | 84 | # Default listening port. 85 | @port 0 86 | 87 | # Actually compile the pipeline into something useable. 88 | @before_compile unquote(__MODULE__) 89 | 90 | @doc """ 91 | 92 | """ 93 | def start_link(options) do 94 | {:ok, _} = run(options) 95 | end 96 | 97 | @doc """ 98 | 99 | """ 100 | def init({transport, :http}, req, _) when transport in [:tcp, :ssl] do 101 | {:upgrade, :protocol, __MODULE__, req, transport} 102 | end 103 | 104 | @doc """ 105 | 106 | """ 107 | def upgrade(req, env, __MODULE__, transport) do 108 | # Generate the Plug connection object from the request. 109 | conn = @connection.conn(req, transport) 110 | try do 111 | # Send the connection to be processed by the pipeline. 112 | # This function is generated in the current module through the 113 | # `__before_compile__` macro. 114 | %{adapter: {@connection, req}} = conn |> pipeline 115 | # Return the result back to cowboy. 116 | {:ok, req, [{:result, :ok} | env]} 117 | catch 118 | :error, value -> 119 | stack = System.stacktrace() 120 | exception = Exception.normalize(:error, value, stack) 121 | reason = {{exception, stack}, conn} 122 | terminate(reason, req, stack) 123 | :throw, value -> 124 | stack = System.stacktrace() 125 | reason = {{{:nocatch, value}, stack}, conn} 126 | terminate(reason, req, stack) 127 | :exit, value -> 128 | stack = System.stacktrace() 129 | reason = {value, conn} 130 | terminate(reason, req, stack) 131 | after 132 | receive do 133 | @already_sent -> :ok 134 | after 135 | 0 -> :ok 136 | end 137 | end 138 | end 139 | 140 | defp terminate(reason, req, stack) do 141 | :cowboy_req.maybe_reply(stack, req) 142 | exit(reason) 143 | end 144 | 145 | defp dispatch() do 146 | # Basically we ignore cowboy's routing mechanism and dispatch everything 147 | # through our pipeline. 148 | :cowboy_router.compile([{:_, [ 149 | {:_, __MODULE__, []}, 150 | ]}]) 151 | end 152 | 153 | @doc """ 154 | Bootstart the cowboy application. Any of the given module attributes can 155 | be overridden as parameters (e.g. port, scheme, etc.). 156 | """ 157 | defp run(options \\ []) do 158 | ref = options |> Keyword.get(:ref, @ref) 159 | acceptors = options |> Keyword.get(:acceptors, @acceptors) 160 | scheme = options |> Keyword.get(:protocol, @protocol) 161 | port = options |> Keyword.get(:port, @port) 162 | 163 | Pipeline.Adapter.Cowboy.ensure_cowboy 164 | apply(:cowboy, :"start_#{scheme}", [ 165 | ref, 166 | acceptors, 167 | [port: port], 168 | [env: [dispatch: dispatch]], 169 | ]) 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pipeline 2 | 3 | Monadic HTTP application composition for [plug] and friends. 4 | 5 | ![build status](http://img.shields.io/travis/metalabdesign/pipeline/master.svg?style=flat) 6 | ![coverage](http://img.shields.io/coveralls/metalabdesign/pipeline/master.svg?style=flat) 7 | ![license](http://img.shields.io/hexpm/l/pipeline.svg?style=flat) 8 | ![version](http://img.shields.io/hexpm/v/pipeline.svg?style=flat) 9 | ![downloads](http://img.shields.io/hexpm/dt/pipeline.svg?style=flat) 10 | 11 | | Feature | Plug | Pipeline | 12 | |------------|------------|------------| 13 | | Composition | Static/Linear | Dynamic/Monadic | 14 | | Guards | Fixed | Extensible | 15 | | Error Handling | Global | Local | 16 | | Control Flow | Dynamic | Static | 17 | | Private Plugs | Yes | No | 18 | 19 | Pipeline was created to address some of the limitations of Plug. Pipeline has equivalent features to Plug and remains fully interoperable with Plug itself – pipelines can both consume and act as plugs. Pipeline is powered by [effects]. 20 | 21 | Pipeline's long-term dream is to be officially incorporated into Plug in some fashion. 22 | 23 | A simple example of using Pipeline: 24 | 25 | ```elixir 26 | defmodule Entry do 27 | # Enable pipeline-related features and conveniences like the `~>` operator. 28 | use Pipeline 29 | 30 | # Your first pipeline! Coming from Plug, `pipeline` works as kind of a hybrid 31 | # version of `init/1` and `call/2`. Instead of configuring the options for 32 | # your connection handler as `init/1` does for Plug, you configure the entire 33 | # processing sequence. 34 | def pipeline do 35 | send(200, "Hello World.") 36 | end 37 | end 38 | 39 | defmodule Server do 40 | @moduledoc """ 41 | Using Pipeline's HTTP adapter instead of Plug's gives a few extra goodies. 42 | They're very similar under the hood, however. This setup will produce a 43 | worker process that you can feed into your application supervisor. 44 | """ 45 | use Pipeline.Adapter.Cowboy, pipeline: Entry.pipeline 46 | end 47 | 48 | defmodule MyApplication do 49 | use Application 50 | 51 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 52 | # for more information on OTP Applications 53 | def start(_type, _args) do 54 | import Supervisor.Spec, warn: false 55 | 56 | children = [ 57 | # Start pipeline-based server. 58 | worker(Server, [[port: 4000]]), 59 | ] 60 | 61 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 62 | # for other strategies and supported options 63 | opts = [strategy: :one_for_one, name: MyApplication.Supervisor] 64 | Supervisor.start_link(children, opts) 65 | end 66 | end 67 | ``` 68 | 69 | More examples can be found in the [/examples] folder. 70 | 71 | ## Usage 72 | 73 | Pipeline provides several type constructors and the standard monadic operators for composing together all your plugs. After you've built your pipeline you feed it through an interpreter to produce a useful result. 74 | 75 | **Type constructors** (build new pipelines): 76 | * `empty` - Create a new empty pipeline. 77 | * `halt` - Disregard all pipelines after this one. 78 | * `match` - Execute pipelines conditionally. 79 | * `plug` - Create a new pipeline that consists of just a single plug. 80 | * `error` - Create an error handler. 81 | 82 | **Connection actions** (conveniences for `plug`): 83 | * `status` - Set response status code. 84 | * `body` - Set response body. 85 | * `set` - Set response header. 86 | * `send` - Send response. 87 | 88 | **Monadic operations** (compose pipelines): 89 | * `~>>` or `bind` - Extend a pipeline based on a new pipeline generated from the previous one. In some ways this is like Elixir's `|>` operator. 90 | * `~>` or `then` - Extend a pipeline with another given pipeline. Just convenient shorthand for `~>>` without depending on the previous pipeline. 91 | * `fmap` or `flat_map` - TODO: Document this. 92 | * `ap` or `apply` - TODO: Document this. 93 | 94 | **Interpreters** (apply actions): 95 | * `conn` - Apply a pipeline to a `Plug.Conn` object. 96 | * `compile` - Compile a pipeline into an Elixir AST. 97 | * `print` - Dump your pipeline to a string. 98 | * `test` - Use for testing your pipelines. 99 | * `transform` - Transform a pipeline (optimize/debug/etc.) 100 | 101 | **Adapters**: 102 | * `cowboy` - Launch a pipeline-based app directly without Plug. 103 | 104 | **DSLs**: 105 | * router - If you like `Plug.Router` this is for you! 106 | 107 | ### Constructors 108 | 109 | Constructors allow you to create new effects for your pipeline. 110 | 111 | Turning plugs into pipelines works exactly as the `plug` macro does. 112 | 113 | ```elixir 114 | pipeline = plug(SomeModule, options) 115 | ``` 116 | 117 | Using halt: 118 | 119 | ```elixir 120 | # my_other_plug will never be invoked. 121 | plug(:my_plug) ~> halt ~> plug(:my_other_plug) 122 | ``` 123 | 124 | Similar in normal plug to: 125 | 126 | ```elixir 127 | def my_other_plug(conn, options) do 128 | conn |> Plug.Conn.halt 129 | end 130 | ``` 131 | 132 | Using match: 133 | 134 | ```elixir 135 | match([ 136 | {:some_matcher, plug(:plug_a)}, 137 | {true, plug(:plug_b)} 138 | ]) 139 | ``` 140 | 141 | ### Monadic Operations 142 | 143 | Pipeline provides all the standard monadic composition mechanisms to manipulate and combine pipelines including `fmap`, `apply` and `bind`. Although handy, knowledge of monads is not required to use these functions. 144 | 145 | `fmap` allows you to rewrite whole pipelines. 146 | 147 | ``` 148 | pipeline = plug(Plug.Static) ~> plug(Plug.Parser) 149 | pipeline |> fmap fn _ -> empty end 150 | ``` 151 | 152 | Using `then` allows you to easily chain those plugs together. 153 | 154 | ```elixir 155 | plug(Plug.Static, to: "/", from: "/") ~> plug(Plug.Parser) 156 | ``` 157 | 158 | This is equivalent to normal Plug's: 159 | 160 | ```elixir 161 | plug Plug.Static, to: "/", from: "/" 162 | plug Plug.Parser 163 | ``` 164 | 165 | Using monadic `bind` allows altering the current pipeline based on the previous pipeline: 166 | 167 | ```elixir 168 | pipelineA ~>> fn effects -> case effects |> Enum.contains(%Effects.Plug) 169 | True -> send(200, "Static plug!") 170 | _ -> empty 171 | end ~> pipelineC 172 | ``` 173 | 174 | 175 | ## Compatibility 176 | 177 | Pipeline aims to exist peacefully and pragmatically in the current Plug ecosystem. Because of some fundamental implementation details within Plug some interoperability is less convenient or performant than it should be; some things Pipeline is capable of (like using anonymous functions) is downright incompatible with Plug and so if you want to be compatible with Plug you need to avoid using these features too. 178 | 179 | There are two mechanisms providing Plug interoperability: `Pipeline.Plug.Export` and `Pipeline.Plug.Import`. 180 | 181 | ### Exports 182 | 183 | Using exports is the least intrusive, most compatible but least performant interoperability mechanism. Using `Pipeline.Plug.Export` generates `init/1` and `call/2` for you from `pipeline/1`. 184 | 185 | Plug works (roughly) by having consumers of your plug calling `init/1` and serializing the result into the AST of the consumer's module. This is designed to optimize the performance of `call/2` since you can do any expensive operations in `init/1`. 186 | 187 | This is problematic for Pipeline because `init/1` _must_ return something compatible with `Macro.escape/1` – pipelines themselves are _not_ AST serializable and so it's not possible to make `init/1` return the pipeline itself. 188 | 189 | Pipeline is fully capable of _compiling_ to an AST, but Plug provides no mechanism to hook into its compilation step and it's not possible to transparently alter module consumers, so this is where we're stuck at as far as providing Pipeline compatibility from within a provider module. 190 | 191 | ```elixir 192 | defmodule MyPlug 193 | use Pipeline 194 | use Pipeline.Plug.Export 195 | 196 | # `pipeline/1` can be private here ensuring your module is indistinguishable 197 | # from any other plug. 198 | defp pipeline(path: path) do 199 | plug(Plug.Static, from: "./public", to: path) 200 | end 201 | 202 | # ---------------------------------------------------------------------------- 203 | # Functions below are generated by `Pipeline.Plug.Export` and are included 204 | # merely for illustrative and documentative purposes. 205 | # ---------------------------------------------------------------------------- 206 | def init(options) do 207 | options 208 | end 209 | 210 | def call(conn, options) do 211 | conn |> Pipeline.interpret(pipeline(options)) 212 | end 213 | end 214 | ``` 215 | 216 | This means you can pass a pipeline anywhere a plug is expected. 217 | 218 | ```elixir 219 | defmodule App do 220 | use Pipeline 221 | 222 | # Generate Plug's `call/2` and `init/1` from `pipeline/1`. 223 | use Pipeline.Plug.Export 224 | 225 | def pipeline(_) do 226 | send(200, "Hello World.") 227 | end 228 | end 229 | 230 | defmodule Server do 231 | @moduledoc """ 232 | Just a standard plug+cowboy server module consuming a Pipeline-based module. 233 | Because Pipeline generates a `call/2` and `init/1` from `pipeline/1` you can 234 | use pipelines everywhere you can use plugs. Fancy. 235 | """ 236 | def start_link() do 237 | {:ok, _} = Plug.Adapters.Cowboy.http App, [] 238 | end 239 | end 240 | ``` 241 | 242 | ### Imports 243 | 244 | If you're willing to explicitly mark that your plug is a pipeline consumer, then there are much greater opportunities for optimization. Imports will scan your list of plugs and detect any of those which are pipelines. Those that are pipelines will be rewritten and compiled. As with exports, `pipeline/1` is the entrypoint. 245 | 246 | ```elixir 247 | defmodule MyPlug 248 | use Pipeline 249 | 250 | # `pipeline/1` _must_ be public here, since it's to be called by the consumer, 251 | # after this module is compiled. 252 | def pipeline(path: path) do 253 | plug(Plug.Static, from: "./public", to: path) 254 | end 255 | end 256 | 257 | defmodule MyApp do 258 | use Pipeline.Plug.Import 259 | use Plug.Builder 260 | 261 | plug MyPipeline, path: "/public" 262 | plug SomeOtherRegularPlug 263 | end 264 | ``` 265 | 266 | 267 | ## Composition 268 | 269 | Plug typically defines its configuration entirely statically – this is partly as a convenience and partly as a mechanism to provide compile-time optimizations. Unfortunately it makes it hard to combine plugs programatically. 270 | 271 | By turning plugs into monads it's possible to do a lot more. Pipelines are, in a way, higher-order plugs. 272 | 273 | ```elixir 274 | defmodule MyPlug do 275 | use Plug.Router 276 | 277 | # This method /foo is always present on this plug no matter what. The 278 | # mechanisms by which you create guards is fixed – you can only match against 279 | # the HTTP method, the verb and the path. 280 | get "/foo" do 281 | conn |> send_resp(200, "Hello World") 282 | end 283 | end 284 | ``` 285 | 286 | ```elixir 287 | defmodule MyMessagePlug do 288 | use Pipeline 289 | 290 | # You can define your own private pipeline generating functions! 291 | def send_message(message) do 292 | send(200, message) 293 | end 294 | 295 | def pipeline({name, message}) do 296 | empty 297 | ~> match([{ 298 | [Match.method(:get), Match.path("/" <> name)], 299 | send_message(message) ~> halt 300 | }]) 301 | end 302 | end 303 | 304 | defmodule MyHelloPlug do 305 | use Pipeline 306 | 307 | def pipeline(name) do 308 | empty 309 | # Pipelines are actually composable! One pipeline can incorporate another 310 | # with values defined at configuration time – something not possible with 311 | # vanilla plugs. 312 | ~> MyMessagePlug.pipeline({name, "Hello " <> name}) 313 | ~> MyMessagePlug.send_message("????") 314 | end 315 | end 316 | 317 | defmodule MyApp do 318 | use Plug.Builder 319 | # Using this behavior to define routes that depend on these parameters is 320 | # something unique to pipelines. 321 | plug MyHelloPlug, "bob" 322 | plug MyHelloPlug, "fred" 323 | end 324 | ``` 325 | 326 | Fancy. 327 | 328 | ### Guards 329 | 330 | The guards in plug come from using `Plug.Router`. 331 | 332 | ```elixir 333 | defmodule MyApp do 334 | # The only possible matching tuple in plug. 335 | get "/", host: "foo.bar." do 336 | conn |> send_resp(200, "is foo") 337 | end 338 | 339 | get "/" do 340 | conn |> send_resp(200, "not foo") 341 | end 342 | end 343 | ``` 344 | 345 | Pipeline's guards are built by composing pipelines and predicates together, similar to plugs `forward:` option 346 | 347 | ```elixir 348 | defmodule MyApp do 349 | use Pipeline 350 | 351 | def pipeline(_) do 352 | match([ 353 | {host("foo.bar."), send(200, "is foo")} 354 | {true, send(200, "not foo")}, 355 | ]) 356 | end 357 | end 358 | ``` 359 | 360 | Most importantly, however, pipeline allows for your own guards as first class citizens. 361 | 362 | ## Error Handling 363 | 364 | Error handling in Pipeline works akin to that of Promises. 365 | 366 | ```elixir 367 | # errors some a and b will be processed, but not from c 368 | a ~> b ~> error(...) ~> c 369 | 370 | ``` 371 | 372 | This is in contrast to Plug's: 373 | 374 | ```elixir 375 | defp handle_errors(foo, bar) do 376 | 377 | end 378 | ``` 379 | 380 | ## Transforming 381 | 382 | Because pipeline is backed by [effects], one only needs to change the pipeline interpreter to entirely change how pipelines are processed. 383 | 384 | ### Optimization 385 | 386 | Plug makes extensive use of Elixir's macro facility to ensure everything runs as fast as possible – pipeline is no different. 387 | 388 | ## Testing 389 | 390 | Using [effects] means that testing pipelines is straightforward. Instead of using the connection interpreter or the compilation interpreter you can use one for testing that doesn't actually do anything. 391 | 392 | TODO: Show how. 393 | 394 | [effects]: https://github.com/metalabdesign/effects 395 | [plug]: https://github.com/elixir-lang/plug 396 | --------------------------------------------------------------------------------