Project architecture boilerplate

This commit is contained in:
mitchell 2019-12-07 21:34:58 -05:00
commit 7ff70d452a
12 changed files with 261 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# 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 third-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").
service-*.tar
# Ignore elixir-ls artifacts
/.elixir_ls/

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
FROM elixir:1.9-slim as build
WORKDIR /root/shortnr/service
COPY . .
RUN mix local.hex --force
RUN mix local.rebar --force
RUN env MIX_ENV=prod mix release
FROM debian:buster-20191014-slim
WORKDIR /home/shortnr
COPY --from=build /root/shortnr/service/_build/prod/rel/service/ .
RUN apt-get update && apt-get install -y --no-install-recommends \
libtinfo5=6.1+20181013-2+deb10u2 \
openssl=1.1.1d-0+deb10u2 \
locales=2.28-10 \
&& rm -rf /var/lib/apt/lists/*
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
RUN groupadd -r shortnr && useradd --no-log-init -r -g shortnr shortnr
RUN chown -R shortnr:shortnr /home/shortnr
USER shortnr
ENTRYPOINT ["bin/service", "start"]
EXPOSE 8080

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
.PHONY: all build clean install start test
build: install test clean
docker build -t shortnr:latest .
clean:
mix clean --deps
install:
mix deps.get
install-prod:
mix deps.get --only prod
start:
iex -S mix run --no-halt
test:
mix test

8
config/config.exs Normal file
View File

@ -0,0 +1,8 @@
import Config
config :service,
port: 8080
config :logger, :console,
format: "date=$date time=$time level=$level$levelpad message=\"$message\" $metadata\n",
metadata: [:port, :file, :line, :crash_reason, :stack]

35
lib/router.ex Normal file
View File

@ -0,0 +1,35 @@
defmodule Shortnr.Router do
use Plug.ErrorHandler
use Plug.Router
require Logger
alias Shortnr.Transport.{Json, Http}
alias Shortnr.Url
plug(Plug.Logger, log: :debug)
plug(:match)
plug(:dispatch)
post "/urls" do
conn
|> Json.decode_request(Url.CreateRequest)
|> Http.handle(&Url.create(&1, Url.Repo.DETS))
|> Json.encode_response()
|> Http.send(:created, conn)
end
match _ do
{:error, {:not_found, "route not found"}, conn}
|> Json.encode_response()
|> Http.send(:ignore, conn)
end
def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do
Logger.error(inspect(reason), stack: stack)
{:error, {:internal_server_error, "internal server error"}, conn}
|> Json.encode_response()
|> Http.send(:ignore, conn)
end
end

28
lib/shortnr.ex Normal file
View File

@ -0,0 +1,28 @@
defmodule Shortnr do
@moduledoc """
Documentation for Shortnr.
"""
use Application
require Logger
@impl true
def start(_type, _args) do
port = port()
children = [
{Plug.Cowboy, scheme: :http, plug: Shortnr.Router, options: [port: port]}
]
Logger.info("server starting", port: port)
Supervisor.start_link(children, strategy: :one_for_one)
end
@spec port() :: integer()
defp port do
case Application.fetch_env(:service, :port) do
{:ok, port} -> port
_ -> 4000
end
end
end

36
lib/transport/http.ex Normal file
View File

@ -0,0 +1,36 @@
defmodule Shortnr.Transport.Http do
import Plug.Conn
@type ok_error :: {:ok, term(), Plug.Conn.t()} | error()
@type error :: {:error, {atom(), String.t()}, Plug.Conn.t()}
@spec handle(ok_error(), (term() -> ok_error())) :: ok_error()
def handle(error = {:error, _sub_error, _conn}, _func), do: error
def handle({:ok, request, conn}, func) do
case func.(request) do
{:ok, response} -> {:ok, response, conn}
{:error, error_value} -> convert_error(error_value, conn)
end
end
defp convert_error({:invalid_argument, message}, conn),
do: {:error, {:bad_request, message}, conn}
defp convert_error(_, conn), do: {:error, {:internal_server_error, "unknown error"}, conn}
@spec send(ok_error(), atom(), Plug.Conn.t()) :: Plug.Conn.t()
def send({:error, {status, body}, _conn}, _success_status, original_conn) do
send_resp(original_conn, status_to_code(status), body)
end
def send({:ok, body, conn}, success_status, _original_conn) do
send_resp(conn, status_to_code(success_status), body)
end
defp status_to_code(:ok), do: 200
defp status_to_code(:created), do: 201
defp status_to_code(:bad_request), do: 400
defp status_to_code(:not_found), do: 404
defp status_to_code(:internal_server_error), do: 500
end

28
lib/transport/json.ex Normal file
View File

@ -0,0 +1,28 @@
defmodule Shortnr.Transport.Json do
import Plug.Conn
alias Shortnr.Transport.Http
@spec decode_request(Plug.Conn.t(), module()) :: Http.ok_error()
def decode_request(conn, struct_module) do
{:ok, body, conn} = read_body(conn)
{:ok, params} = Jason.decode(body)
params_list =
params
|> Map.to_list()
|> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)
{:ok, struct(struct_module, params_list), conn}
end
@spec encode_response(Http.ok_error()) :: Http.ok_error()
def encode_response({:error, {status, response}, conn}) do
{:ok, json_body} = Jason.encode(%{data: response})
{:error, {status, json_body}, conn}
end
def encode_response({:ok, response, conn}) do
{:ok, json_body} = Jason.encode(%{data: response})
{:ok, json_body, conn}
end
end

View File

@ -0,0 +1,4 @@
defmodule Shortnr.Transport do
@type error :: {:error, {atom(), String.t()}}
@type ok_error :: {:ok, term()} | error()
end

32
mix.exs Normal file
View File

@ -0,0 +1,32 @@
defmodule Shortnr.MixProject do
use Mix.Project
def project do
[
app: :service,
version: "0.1.0",
elixir: "~> 1.9",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
mod: {Shortnr, []},
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:plug_cowboy, "~> 2.0"},
{:elixir_uuid, "~> 1.2"},
{:jason, "~> 1.1"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

11
mix.lock Normal file
View File

@ -0,0 +1,11 @@
%{
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm"},
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
}

1
test/test_helper.exs Normal file
View File

@ -0,0 +1 @@
ExUnit.start()