Initial commit
This commit is contained in:
commit
11836583b1
11 changed files with 529 additions and 0 deletions
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
micropub_plug-*.tar
|
||||||
|
|
29
LICENSE
Normal file
29
LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2018, Ryan Johnson
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
48
README.md
Normal file
48
README.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# PlugMicropub
|
||||||
|
|
||||||
|
A small library for helping build a Plug-based Micropub server.
|
||||||
|
|
||||||
|
A basic example server that implements all [Micropub Rocks!][1] validation
|
||||||
|
tests can be found [here][2].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Basic Usage:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
plug Plug.Parsers,
|
||||||
|
parsers: [:urlencoded, :multipart, :json],
|
||||||
|
pass: ["*/*"],
|
||||||
|
json_decoder: Poison
|
||||||
|
|
||||||
|
plug PlugMicropub,
|
||||||
|
handler: MyApp.MicropubHandler,
|
||||||
|
json_decoder: Poison
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forwarding
|
||||||
|
|
||||||
|
If you want `PlugMicropub` to serve only a particular route, configure your router like:
|
||||||
|
|
||||||
|
#### Plug.Router
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
forward "/micropub",
|
||||||
|
to: PlugMicropub,
|
||||||
|
init_opts: [
|
||||||
|
handler: MyApp.MicropubHandler,
|
||||||
|
json_decoder: Poison
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Phoenix.Router
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
forward "/micropub",
|
||||||
|
PlugMicropub,
|
||||||
|
handler: MyApp.MicropubHandler,
|
||||||
|
json_decoder: Poison
|
||||||
|
```
|
||||||
|
|
||||||
|
[1]: https://micropub.rocks/
|
||||||
|
[2]: https://github.com/bismark/micropub-example
|
1
config/config.exs
Normal file
1
config/config.exs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use Mix.Config
|
322
lib/plug_micropub.ex
Normal file
322
lib/plug_micropub.ex
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
defmodule PlugMicropub do
|
||||||
|
@moduledoc """
|
||||||
|
A Plug for building a Micropub server.
|
||||||
|
|
||||||
|
To use:
|
||||||
|
|
||||||
|
"""
|
||||||
|
use Plug.Router
|
||||||
|
|
||||||
|
plug(:match)
|
||||||
|
plug(:dispatch)
|
||||||
|
|
||||||
|
# Plug Callbacks
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def init(opts) do
|
||||||
|
handler =
|
||||||
|
Keyword.get(opts, :handler) || raise ArgumentError, "Micropub Plug requires :handler option"
|
||||||
|
|
||||||
|
json_encoder =
|
||||||
|
Keyword.get(opts, :json_encoder) ||
|
||||||
|
raise ArgumentError, "Micropub Plug requires :json_encoder option"
|
||||||
|
|
||||||
|
[handler: handler, json_encoder: json_encoder]
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def call(conn, opts) do
|
||||||
|
conn = put_private(conn, :plug_micropub, opts)
|
||||||
|
super(conn, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
|
||||||
|
post "/" do
|
||||||
|
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||||
|
{:ok, action, conn} <- get_action(conn) do
|
||||||
|
handle_action(action, access_token, conn)
|
||||||
|
else
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/" do
|
||||||
|
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||||
|
{:ok, query} <- get_query(conn) do
|
||||||
|
handle_query(query, access_token, conn)
|
||||||
|
else
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/media" do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||||
|
{:ok, file} <- get_file(conn),
|
||||||
|
{:ok, url} <- handler.handle_media(file, access_token) do
|
||||||
|
conn
|
||||||
|
|> put_resp_header("location", url)
|
||||||
|
|> send_resp(:created, "")
|
||||||
|
else
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Internal Functions
|
||||||
|
|
||||||
|
defp send_content(conn, content) do
|
||||||
|
json_encoder = conn.private[:plug_micropub][:json_encoder]
|
||||||
|
body = json_encoder.encode!(content)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(:ok, body)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_error(conn, {:error, error}) do
|
||||||
|
body = %{error: error}
|
||||||
|
_send_error(conn, body)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_error(conn, {:error, error, description}) do
|
||||||
|
body = %{error: error, error_description: description}
|
||||||
|
_send_error(conn, body)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp _send_error(conn, body) do
|
||||||
|
json_encoder = conn.private[:plug_micropub][:json_encoder]
|
||||||
|
|
||||||
|
code = get_error_code(body.error)
|
||||||
|
body = json_encoder.encode!(body)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(code, body)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_error_code(:insufficient_scope), do: :unauthorized
|
||||||
|
defp get_error_code(:invalid_request), do: :bad_request
|
||||||
|
defp get_error_code(code), do: code
|
||||||
|
|
||||||
|
defp get_action(conn) do
|
||||||
|
{action, body_params} = Map.pop(conn.body_params, "action")
|
||||||
|
conn = %Plug.Conn{conn | body_params: body_params}
|
||||||
|
|
||||||
|
case action do
|
||||||
|
nil ->
|
||||||
|
{:ok, :create, conn}
|
||||||
|
|
||||||
|
action when action in ["delete", "undelete", "update"] ->
|
||||||
|
{:ok, String.to_existing_atom(action), conn}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :invalid_request}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_query(conn) do
|
||||||
|
case Map.fetch(conn.query_params, "q") do
|
||||||
|
{:ok, query} when query in ["config", "source", "syndicate-to"] ->
|
||||||
|
{:ok, String.to_existing_atom(query)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :invalid_request}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_file(conn) do
|
||||||
|
case Map.fetch(conn.body_params, "file") do
|
||||||
|
{:ok, file} -> {:ok, file}
|
||||||
|
:error -> {:error, :invalid_request}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_access_token(conn) do
|
||||||
|
{access_token, body_params} = Map.pop(conn.body_params, "access_token")
|
||||||
|
conn = %Plug.Conn{conn | body_params: body_params}
|
||||||
|
|
||||||
|
case access_token do
|
||||||
|
nil -> parse_auth_header(conn)
|
||||||
|
access_token -> {:ok, access_token, conn}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_auth_header(conn) do
|
||||||
|
with [header] <- get_req_header(conn, "authorization"),
|
||||||
|
_ = IO.inspect(header),
|
||||||
|
"Bearer" <> token <- header,
|
||||||
|
do: {:ok, String.trim(token), conn},
|
||||||
|
else: (_ -> {:error, :unauthorized})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_action(:create, access_token, conn) do
|
||||||
|
content_type = conn |> get_req_header("content-type") |> List.first()
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
with {:ok, type, properties} <- parse_create_body(content_type, conn.body_params),
|
||||||
|
{:ok, code, url} <- handler.handle_create(type, properties, access_token) do
|
||||||
|
conn
|
||||||
|
|> put_resp_header("location", url)
|
||||||
|
|> send_resp(code, "")
|
||||||
|
else
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_action(:update, access_token, conn) do
|
||||||
|
content_type = conn |> get_req_header("content-type") |> List.first()
|
||||||
|
|
||||||
|
with "application/json" <- content_type,
|
||||||
|
{url, properties} when is_binary(url) <- Map.pop(conn.body_params, "url"),
|
||||||
|
{:ok, replace, add, delete} <- parse_update_properties(properties),
|
||||||
|
do: do_update(conn, access_token, url, replace, add, delete),
|
||||||
|
else: (_ -> send_error(conn, {:error, :invalid_request}))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_action(:delete, access_token, conn) do
|
||||||
|
with {:ok, url} <- Map.fetch(conn.body_params, "url"),
|
||||||
|
do: do_delete(conn, access_token, url),
|
||||||
|
else: (_ -> send_error(conn, {:error, :invalid_request}))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_action(:undelete, access_token, conn) do
|
||||||
|
with {:ok, url} <- Map.fetch(conn.body_params, "url"),
|
||||||
|
do: do_undelete(conn, access_token, url),
|
||||||
|
else: (_ -> send_error(conn, {:error, :invalid_request}))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_query(:config, access_token, conn) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
case handler.handle_config_query(access_token) do
|
||||||
|
{:ok, content} -> send_content(conn, content)
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_query(:source, access_token, conn) do
|
||||||
|
with {:ok, url} <- Map.fetch(conn.query_params, "url"),
|
||||||
|
do: do_source_query(conn, access_token, url),
|
||||||
|
else: (_ -> send_error(conn, {:error, :invalid_request}))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_query(:"syndicate-to", access_token, conn) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
case handler.handle_syndicate_to_query(access_token) do
|
||||||
|
{:ok, content} -> send_content(conn, content)
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_update_properties(properties) do
|
||||||
|
properties = Map.take(properties, ["replace", "add", "delete"])
|
||||||
|
|
||||||
|
valid? =
|
||||||
|
Enum.all?(properties, fn
|
||||||
|
{"delete", prop} when is_list(prop) ->
|
||||||
|
Enum.all?(prop, &is_binary/1)
|
||||||
|
|
||||||
|
{_k, prop} when is_map(prop) ->
|
||||||
|
Enum.all?(prop, fn
|
||||||
|
{_k, v} when is_list(v) -> true
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end)
|
||||||
|
|
||||||
|
if valid? do
|
||||||
|
replace = Map.get(properties, "replace", %{})
|
||||||
|
add = Map.get(properties, "add", %{})
|
||||||
|
delete = Map.get(properties, "delete", %{})
|
||||||
|
{:ok, replace, add, delete}
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_update(conn, access_token, url, replace, add, delete) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
case handler.handle_update(url, replace, add, delete, access_token) do
|
||||||
|
:ok ->
|
||||||
|
send_resp(conn, :no_content, "")
|
||||||
|
|
||||||
|
{:ok, url} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_header("location", url)
|
||||||
|
|> send_resp(:created, "")
|
||||||
|
|
||||||
|
error ->
|
||||||
|
send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_delete(conn, access_token, url) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
case handler.handle_delete(url, access_token) do
|
||||||
|
:ok -> send_resp(conn, :no_content, "")
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_undelete(conn, access_token, url) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
|
||||||
|
case handler.handle_undelete(url, access_token) do
|
||||||
|
:ok ->
|
||||||
|
send_resp(conn, :no_content, "")
|
||||||
|
|
||||||
|
{:ok, url} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_header("location", url)
|
||||||
|
|> send_resp(:created, "")
|
||||||
|
|
||||||
|
error ->
|
||||||
|
send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_source_query(conn, access_token, url) do
|
||||||
|
handler = conn.private[:plug_micropub][:handler]
|
||||||
|
properties = Map.get(conn.query_params, "properties", [])
|
||||||
|
|
||||||
|
case handler.handle_source_query(url, properties, access_token) do
|
||||||
|
{:ok, content} -> send_content(conn, content)
|
||||||
|
error -> send_error(conn, error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_create_body("application/json", params) do
|
||||||
|
with {:ok, ["h-" <> type]} <- Map.fetch(params, "type"),
|
||||||
|
{:ok, properties} when is_map(properties) <- Map.fetch(params, "properties") do
|
||||||
|
properties =
|
||||||
|
properties
|
||||||
|
|> Enum.reject(&match?({"mp-" <> _, _}, &1))
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
{:ok, type, properties}
|
||||||
|
else
|
||||||
|
_ -> {:error, :invalid_request}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_create_body(_, params) do
|
||||||
|
with {type, params} when is_binary(type) <- Map.pop(params, "h") do
|
||||||
|
properties =
|
||||||
|
params
|
||||||
|
|> Enum.reject(&match?({"mp-" <> _, _}, &1))
|
||||||
|
|> Enum.map(fn {k, v} -> {k, List.wrap(v)} end)
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
{:ok, type, properties}
|
||||||
|
else
|
||||||
|
_ -> {:error, :invalid_request}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
54
lib/plug_micropub/handler_behaviour.ex
Normal file
54
lib/plug_micropub/handler_behaviour.ex
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
defmodule PlugMicropub.HandlerBehaviour do
|
||||||
|
@moduledoc """
|
||||||
|
Behaviour defining the interface for a PlugMicropub Handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type access_token :: String.t()
|
||||||
|
@type handler_error_atom :: :invalid_request | :forbidden | :insufficient_scope
|
||||||
|
@type handler_error ::
|
||||||
|
{:error, handler_error_atom} | {:error, handler_error_atom, description :: String.t()}
|
||||||
|
|
||||||
|
@callback handle_create(type :: String.t(), properties :: map, access_token) ::
|
||||||
|
{:ok, :created | :accepted, url :: String.t()}
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_update(
|
||||||
|
url :: String.t(),
|
||||||
|
replace :: map,
|
||||||
|
add :: map,
|
||||||
|
delete :: map,
|
||||||
|
access_token
|
||||||
|
) ::
|
||||||
|
:ok
|
||||||
|
| {:ok, url :: String.t()}
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_delete(url :: String.t(), access_token) ::
|
||||||
|
:ok
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_undelete(url :: String.t(), access_token) ::
|
||||||
|
:ok
|
||||||
|
| {:ok, url :: String.t()}
|
||||||
|
| {:error, handler_error}
|
||||||
|
| {:error, handler_error, error_description :: String.t()}
|
||||||
|
|
||||||
|
@callback handle_config_query(access_token) ::
|
||||||
|
{:ok, map}
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_config_query(access_token) ::
|
||||||
|
{:ok, map}
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_source_query(
|
||||||
|
url :: String.t(),
|
||||||
|
properties :: [String.t()],
|
||||||
|
access_token
|
||||||
|
) ::
|
||||||
|
{:ok, map}
|
||||||
|
| handler_error
|
||||||
|
|
||||||
|
@callback handle_media(file :: Plug.Upload.t(), access_token) ::
|
||||||
|
{:ok, url :: String.t()} | handler_error
|
||||||
|
end
|
36
mix.exs
Normal file
36
mix.exs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule PlugMicropub.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :plug_micropub,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.6",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps(),
|
||||||
|
name: "PlugMicropub",
|
||||||
|
description: "A small library for building a Plug-based Micropub server.",
|
||||||
|
source_url: "https://github.com/bismark/plug_micropub",
|
||||||
|
docs: [main: "readme", extras: ["README.md"]],
|
||||||
|
package: [
|
||||||
|
name: "plug_micropub",
|
||||||
|
licenses: ["BSD 3-Clause"],
|
||||||
|
maintainers: ["Ryan Johnson"],
|
||||||
|
links: %{github: "https://github.com/bismark/plug_micropub"}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
{:plug, "~> 1.5"},
|
||||||
|
{:ex_doc, "~> 0.18.3", only: :dev, runtime: false}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
6
mix.lock
Normal file
6
mix.lock
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
%{
|
||||||
|
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"},
|
||||||
|
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
|
||||||
|
"plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
}
|
4
test/plug_micropub_test.exs
Normal file
4
test/plug_micropub_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule PlugMicropubTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest PlugMicropub
|
||||||
|
end
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
Loading…
Reference in a new issue