Skip to content

Commit c120559

Browse files
committed
Init
0 parents  commit c120559

13 files changed

+543
-0
lines changed

.formatter.exs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
locals_without_parens: [
3+
dynamic_plug: 1,
4+
dynamic_plug: 2,
5+
dynamic_plug: 3
6+
],
7+
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
8+
]

.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
plug_dynamic-*.tar
24+
25+
# Library Stuff
26+
/mix.lock

.travis.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
language: elixir
2+
sudo: false
3+
env:
4+
- MIX_ENV=test
5+
elixir:
6+
- 1.6
7+
- 1.7
8+
otp_release:
9+
- 21.0
10+
script: mix coveralls.travis
11+
cache:
12+
directories:
13+
- ~/.mix
14+
- ~/.hex
15+
- _build
16+
jobs:
17+
include:
18+
- stage: format
19+
env:
20+
- MIX_ENV=dev
21+
script: mix format --check-formatted
22+
elixir: 1.7
23+
- stage: credo
24+
env:
25+
- MIX_ENV=dev
26+
script: mix credo --strict
27+
elixir: 1.7
28+
- stage: dialyzer
29+
env:
30+
- MIX_ENV=dev
31+
before_script: travis_wait mix dialyzer --plt
32+
script: mix dialyzer --halt-exit-status
33+
elixir: 1.7
34+
- stage: inch
35+
env:
36+
- MIX_ENV=docs
37+
script: mix inch.report
38+
elixir: 1.7

LICENSE

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The MIT License (MIT)
2+
Copyright (c) 2018, JOSHMARTIN GmbH, Jonatan Männchen
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5+
6+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7+
8+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Plug Dynamic
2+
3+
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/jshmrtn/plug-dynamic/master/LICENSE)
4+
[![Build Status](https://travis-ci.org/jshmrtn/plug-dynamic.svg?branch=master)](https://travis-ci.org/jshmrtn/plug-dynamic)
5+
[![Hex.pm Version](https://img.shields.io/hexpm/v/plug_dynamic.svg?style=flat)](https://hex.pm/packages/plug_dynamic)
6+
[![InchCI](https://inch-ci.org/github/jshmrtn/plug-dynamic.svg?branch=master)](https://inch-ci.org/github/jshmrtn/plug-dynamic)
7+
[![Coverage Status](https://coveralls.io/repos/github/jshmrtn/plug-dynamic/badge.svg?branch=master)](https://coveralls.io/github/jshmrtn/plug-dynamic?branch=master)
8+
9+
10+
Allows registration of every Plug with dynamic configuration.
11+
12+
## Installation
13+
14+
The package can be installed by adding `plug_dynamic` to your list of
15+
dependencies in `mix.exs`:
16+
17+
```elixir
18+
def deps do
19+
[
20+
{:plug_dynamic, "~> 1.0"}
21+
]
22+
end
23+
```
24+
25+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
26+
and published on [HexDocs](https://hexdocs.pm). The docs can be found at
27+
[https://hexdocs.pm/plug_dynamic](https://hexdocs.pm/plug_dynamic).

lib/plug_dynamic.ex

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
defmodule PlugDynamic do
2+
@moduledoc """
3+
Moves any plugs configuration from compile time to runtime.
4+
5+
### Usage Example (plain)
6+
7+
defmodule Acme.Endpoint do
8+
use Plug.Builder
9+
10+
plug(
11+
PlugDynamic,
12+
plug: Plug.IpWhitelist.IpWhitelistEnforcer,
13+
options: {__MODULE__, :ip_whitelist_options, 0},
14+
reevaluate: :first_usage
15+
)
16+
17+
def ip_whitelist_options,
18+
do: Application.fetch_env!(:acme, Plug.IpWhitelist.IpWhitelistEnforcer)
19+
end
20+
21+
### Usage Example (macro)
22+
23+
defmodule Acme.Endpoint do
24+
use Plug.Builder
25+
use PlugDynamic
26+
27+
dynamic_plug Plug.IpWhitelist.IpWhitelistEnforcer, [reevaluate: :first_usage] do
28+
Application.fetch_env!(:acme, Plug.IpWhitelist.IpWhitelistEnforcer)
29+
end
30+
end
31+
32+
### Options
33+
34+
* `options` - anonymous function or `{Module, :function, [args]}` tuple to fetch the configuration
35+
* `reevaluate` - default: `:first_usage` - one of the following values
36+
- `:first_usage` - Evaluate Options when it is used for the first time. The resulting value will be cached in ets.
37+
- `:always` - Evaluate Options for every request. (Attention: This can cause a severe performance impact.)
38+
39+
"""
40+
41+
@behaviour Plug
42+
43+
alias Plug.Conn
44+
alias PlugDynamic.{Builder, Storage}
45+
46+
require Logger
47+
48+
@type options_fetcher :: (() -> any) | {atom, atom, [any]} | mfa
49+
@type reevaluate :: :first_usage | :always
50+
@type plug :: atom
51+
52+
@typep options_fetcher_normalized :: (() -> any)
53+
54+
@enforce_keys [:options_fetcher, :reevaluate, :plug, :reference]
55+
defstruct @enforce_keys
56+
57+
defmacro __using__ do
58+
quote do
59+
import unquote(Builder), only: [dynamic_plug: 1, dynamic_plug: 2, dynamic_plug: 3]
60+
end
61+
end
62+
63+
@impl Plug
64+
@doc false
65+
def init(options) do
66+
plug = Keyword.fetch!(options, :plug)
67+
68+
%__MODULE__{
69+
plug: plug,
70+
options_fetcher: options |> Keyword.get(:options, {__MODULE__, :empty_opts, 0}),
71+
reevaluate: options |> Keyword.get(:reevaluate, :first_usage),
72+
reference: :"#{plug}.#{inspect(make_ref())}"
73+
}
74+
end
75+
76+
@impl Plug
77+
@doc false
78+
def call(%Conn{} = conn, %{reevaluate: :always, plug: plug, options_fetcher: options_fetcher}),
79+
do: plug.call(conn, plug.init(normalize_options_fetcher(options_fetcher).()))
80+
81+
def call(%Conn{} = conn, %{
82+
reevaluate: :first_usage,
83+
plug: plug,
84+
options_fetcher: options_fetcher,
85+
reference: reference
86+
}) do
87+
options = fetch_or_create_options(plug, reference, options_fetcher)
88+
plug.call(conn, options)
89+
end
90+
91+
@spec fetch_or_create_options(
92+
plug :: atom,
93+
reference :: atom,
94+
options_fetcher :: options_fetcher
95+
) :: any
96+
defp fetch_or_create_options(plug, reference, options_fetcher) do
97+
reference
98+
|> Storage.fetch()
99+
|> case do
100+
{:ok, options} ->
101+
options
102+
103+
:error ->
104+
options = plug.init(normalize_options_fetcher(options_fetcher).())
105+
106+
Logger.debug(fn ->
107+
"Options for Plug `#{inspect(plug)}` (#{inspect(reference, pretty: true)}) not found, storing"
108+
end)
109+
110+
Storage.store(reference, options)
111+
112+
options
113+
end
114+
end
115+
116+
@spec normalize_options_fetcher(fun :: options_fetcher) :: options_fetcher_normalized
117+
defp normalize_options_fetcher(fun) when is_function(fun, 0), do: fun
118+
119+
defp normalize_options_fetcher(fun) when is_function(fun),
120+
do: raise("Option fetching function must have 0 arity")
121+
122+
defp normalize_options_fetcher({module, function, arguments})
123+
when is_atom(module) and is_atom(function) and is_list(arguments),
124+
do: fn -> apply(module, function, arguments) end
125+
126+
if function_exported?(Function, :capture, 2) do
127+
defp normalize_options_fetcher({module, function, 0})
128+
when is_atom(module) and is_atom(function),
129+
do: Function.capture(module, function, 0)
130+
else
131+
defp normalize_options_fetcher({module, function, 0})
132+
when is_atom(module) and is_atom(function),
133+
do: fn -> apply(module, function, []) end
134+
end
135+
136+
defp normalize_options_fetcher({module, function, arity})
137+
when is_atom(module) and is_atom(function) and is_integer(0) and arity > 0,
138+
do: raise("Option fetching function must have 0 arity")
139+
140+
@doc false
141+
@spec empty_opts :: []
142+
def empty_opts, do: []
143+
end

lib/plug_dynamic/application.ex

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule PlugDynamic.Application do
2+
@moduledoc false
3+
4+
use Application
5+
6+
alias PlugDynamic.Storage
7+
8+
def start(_type, _args),
9+
do:
10+
Supervisor.start_link(
11+
[Storage],
12+
strategy: :one_for_one,
13+
name: PlugDynamic.Supervisor
14+
)
15+
end

lib/plug_dynamic/builder.ex

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule PlugDynamic.Builder do
2+
@moduledoc """
3+
Exposes Plug Builder Macros
4+
"""
5+
6+
defmacro dynamic_plug(plug, config \\ [], options \\ [])
7+
8+
defmacro dynamic_plug(plug, [do: block], []) do
9+
config = [plug: plug]
10+
{fun_definition, ref} = inplace_config(plug, block)
11+
12+
[
13+
fun_definition,
14+
quote bind_quoted: [config: config, plug: plug, ref: ref] do
15+
plug(PlugDynamic, config ++ [options: {__MODULE__, :__dynamic_plug_config, [plug, ref]}])
16+
end
17+
]
18+
end
19+
20+
defmacro dynamic_plug(plug, config, do: block) do
21+
config = Keyword.put_new(config, :plug, plug)
22+
{fun_definition, ref} = inplace_config(plug, block)
23+
24+
[
25+
fun_definition,
26+
quote bind_quoted: [config: config, plug: plug, ref: ref] do
27+
plug(PlugDynamic, config ++ [options: {__MODULE__, :__dynamic_plug_config, [plug, ref]}])
28+
end
29+
]
30+
end
31+
32+
defmacro dynamic_plug(plug, config, []) do
33+
config = Keyword.put_new(config, :plug, plug)
34+
35+
quote bind_quoted: [config: config] do
36+
plug(PlugDynamic, config)
37+
end
38+
end
39+
40+
defp inplace_config(plug, block) do
41+
ref = :"#{inspect(make_ref())}"
42+
43+
fun_definition =
44+
quote do
45+
@doc false
46+
def __dynamic_plug_config(unquote(plug), unquote(ref)) do
47+
unquote(block)
48+
end
49+
end
50+
51+
{fun_definition, ref}
52+
end
53+
end

lib/plug_dynamic/storage.ex

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule PlugDynamic.Storage do
2+
@moduledoc false
3+
# Store Options so that they do not have to be re-evaluated
4+
5+
use GenServer
6+
7+
@server __MODULE__
8+
9+
@type t :: %__MODULE__{
10+
table_name: atom
11+
}
12+
13+
@enforce_keys [:table_name]
14+
defstruct @enforce_keys
15+
16+
@doc false
17+
@spec start_link(options :: Keyword.t()) :: GenServer.on_start()
18+
def start_link(options),
19+
do: GenServer.start_link(__MODULE__, options, name: Keyword.get(options, :name, @server))
20+
21+
@doc false
22+
@impl GenServer
23+
@spec init(options :: Keyword.t()) :: {:ok, t}
24+
def init(options),
25+
do:
26+
{:ok,
27+
%__MODULE__{
28+
table_name: options |> Keyword.get(:name, @server) |> table_name |> ets_table
29+
}}
30+
31+
@doc false
32+
@impl GenServer
33+
@spec handle_cast(request :: {:store, reference :: atom, options :: any}, t) :: {:noreply, t}
34+
def handle_cast({:store, reference, options}, %__MODULE__{table_name: table_name} = state) do
35+
:ets.insert(table_name, {reference, options})
36+
{:noreply, state}
37+
end
38+
39+
@doc false
40+
@spec fetch(server :: GenServer.name(), reference :: atom) :: {:ok, any} | :error
41+
def fetch(server \\ @server, reference) when is_atom(reference) do
42+
server
43+
|> table_name
44+
|> :ets.lookup(reference)
45+
|> case do
46+
[{^reference, options}] -> {:ok, options}
47+
_ -> :error
48+
end
49+
end
50+
51+
@doc false
52+
@spec store(server :: GenServer.name(), reference :: atom, options :: any) :: :ok
53+
def store(server \\ @server, reference, options) when is_atom(reference),
54+
do: GenServer.cast(server, {:store, reference, options})
55+
56+
@spec ets_table(table_name :: atom) :: atom
57+
defp ets_table(table_name), do: :ets.new(table_name, [:protected, :ordered_set, :named_table])
58+
59+
@spec table_name(server :: GenServer.name()) :: atom
60+
defp table_name(server)
61+
defp table_name(server) when is_atom(server), do: Module.concat(server, Table)
62+
63+
defp table_name({:global, server}) when is_atom(server),
64+
do: {:global, Module.concat(server, Table)}
65+
66+
defp table_name({:via, _, server}) when is_atom(server),
67+
do: raise("Server is not supported with :via name")
68+
end

0 commit comments

Comments
 (0)