diff --git a/.gitignore b/.gitignore index de44667..baf5cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,9 @@ birdy_chat-*.tar # Ignore digested assets cache. /priv/static/cache_manifest.json +# Ignore messages folder +/priv/messages/ + # In case you use Node.js/npm, you want to ignore these. npm-debug.log /assets/node_modules/ - diff --git a/lib/birdy_chat/application.ex b/lib/birdy_chat/application.ex index 0187807..f53b284 100644 --- a/lib/birdy_chat/application.ex +++ b/lib/birdy_chat/application.ex @@ -5,11 +5,14 @@ defmodule BirdyChat.Application do use Application + @env Mix.env + @impl true def start(_type, _args) do children = [ BirdyChatWeb.Telemetry, BirdyChat.Repo, + {BirdyChat.Identity, env: @env}, {DNSCluster, query: Application.get_env(:birdy_chat, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: BirdyChat.PubSub}, # Start a worker by calling: BirdyChat.Worker.start_link(arg) diff --git a/lib/birdy_chat/identity.ex b/lib/birdy_chat/identity.ex new file mode 100644 index 0000000..caf8c54 --- /dev/null +++ b/lib/birdy_chat/identity.ex @@ -0,0 +1,56 @@ +defmodule BirdyChat.Identity do + use Agent + + defstruct [:identity, :peers, :mode] + + def start_link(opts) do + name = opts[:name] || __MODULE__ + input = opts[:input] + + Agent.start_link(fn -> initialise(input) end, name: name) + end + + def mode(pid \\ __MODULE__) do + Agent.get(pid, & &1.mode) + end + + def identity(pid \\ __MODULE__) do + Agent.get(pid, & &1.identity) + end + + def peers(pid \\ __MODULE__) do + Agent.get(pid, & &1.peers) + end + + def parse_identity(value) do + case Integer.parse(value) do + {_number, ""} -> value + _ -> raise "Identity must be a string that can be converted to Integer, got: >>#{value}<<." + end + end + + def parse_peers(peers) do + values = String.split(peers, ";") + + for value <- values, + [identity, address] = String.split(value, "::"), into: %{} do + {identity, address} + end + end + + def initialise(%{peers: peers, identity: identity}) do + peers = parse_peers(peers) + identity = parse_identity(identity) + + %__MODULE__{identity: identity, peers: peers, mode: :connected} + end + + def initialise(nil) do + identity = System.get_env("BIRDY_CHAT_IDENTITY") + peers = System.get_env("BIRDY_CHAT_PEERS") + + case {identity, peers} do + {nil, nil} -> %__MODULE__{identity: "test", peers: [], mode: :singleton} + end + end +end diff --git a/lib/birdy_chat/message.ex b/lib/birdy_chat/message.ex new file mode 100644 index 0000000..43efc56 --- /dev/null +++ b/lib/birdy_chat/message.ex @@ -0,0 +1,46 @@ +defmodule BirdyChat.Message do + use Ecto.Schema + + embedded_schema do + field :from, :string + field :to, :string + field :message, :string + end + + def validate(params) do + changeset = + %__MODULE__{} + |> Ecto.Changeset.cast(params, [:from, :to, :message]) + |> Ecto.Changeset.validate_required([:from, :to, :message]) + + if changeset.valid? do + {:ok, changeset} + else + {:error, changeset} + end + end + + def write(%{to: to, from: from, message: message} = msg) do + message_to_write = "#{from}: #{message}\n" + :ok = create_messages_folder!(Application.app_dir(:birdy_chat, ["priv", "messages"])) + path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{to}.txt"]) + result = File.write!(path, message_to_write, [:append]) + {:ok, msg} + end + + def create_messages_folder!(path) do + case File.mkdir(path) do + :ok -> + :ok + + {:error, :eexist} -> + :ok + + {:error, reason} -> + raise File.Error, + reason: reason, + action: "make directory", + path: IO.chardata_to_string(path) + end + end +end diff --git a/lib/birdy_chat_web/api/messages/controller.ex b/lib/birdy_chat_web/api/messages/controller.ex new file mode 100644 index 0000000..7c6531a --- /dev/null +++ b/lib/birdy_chat_web/api/messages/controller.ex @@ -0,0 +1,20 @@ +defmodule BirdyChatWeb.Api.Messages.Controller do + use BirdyChatWeb, :controller + + def create(conn, params) do + case BirdyChat.Message.validate(params) do + {:ok, changeset} -> + case BirdyChat.Message.write(changeset.changes) do + {:ok, msg} -> + conn + |> put_status(:created) + |> render(:create, message: msg) + end + + {:error, changeset} -> + conn + |> put_status(:unprocessable_entity) + |> render(:error, changeset: changeset) + end + end +end diff --git a/lib/birdy_chat_web/api/messages/json.ex b/lib/birdy_chat_web/api/messages/json.ex new file mode 100644 index 0000000..a238764 --- /dev/null +++ b/lib/birdy_chat_web/api/messages/json.ex @@ -0,0 +1,14 @@ +defmodule BirdyChatWeb.Api.Messages.JSON do + def render("create.json", %{message: message}) do + message + end + + def render("error.json", %{changeset: changeset}) do + errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1) + %{errors: errors} + end + + def get_error({msg, opts}) do + Gettext.dgettext(BirdyChatWeb.Gettext, "errors", msg, opts) + end +end diff --git a/lib/birdy_chat_web/router.ex b/lib/birdy_chat_web/router.ex index 882b44e..0dc6d3b 100644 --- a/lib/birdy_chat_web/router.ex +++ b/lib/birdy_chat_web/router.ex @@ -20,6 +20,12 @@ defmodule BirdyChatWeb.Router do get "/", PageController, :home end + scope "/api", BirdyChatWeb.Api do + pipe_through [:api] + + post "/messages", Messages.Controller, :create + end + # Other scopes may use custom stacks. # scope "/api", BirdyChatWeb do # pipe_through :api diff --git a/test/birdy_chat/identity_test.exs b/test/birdy_chat/identity_test.exs new file mode 100644 index 0000000..33bd710 --- /dev/null +++ b/test/birdy_chat/identity_test.exs @@ -0,0 +1,29 @@ +defmodule BirdyChat.IdentityTest do + use BirdyChat.DataCase + + setup do + registry = start_supervised!({Registry, keys: :unique, name: __MODULE__}) + %{registry: registry} + end + + describe "process" do + test "can be started with a new name" do + name = {:via, Registry, {__MODULE__, "identity"}} + {:ok, process} = BirdyChat.Identity.start_link(name: name) + mode = BirdyChat.Identity.mode(process) + assert mode == :singleton + end + + test "can be started by giving values like environment variables" do + name = {:via, Registry, {__MODULE__, "identity"}} + input = %{peers: "13::http://localhost:4001;14::http://localhost:4002", identity: "12"} + {:ok, process} = BirdyChat.Identity.start_link(name: name, input: input) + + identity = BirdyChat.Identity.identity(process) + assert identity == "12" + + peers = BirdyChat.Identity.peers(process) + assert peers == %{"13" => "http://localhost:4001", "14" => "http://localhost:4002"} + end + end +end diff --git a/test/birdy_chat_web/api/messages_test.exs b/test/birdy_chat_web/api/messages_test.exs new file mode 100644 index 0000000..b9d618c --- /dev/null +++ b/test/birdy_chat_web/api/messages_test.exs @@ -0,0 +1,40 @@ +defmodule BirdyChatWeb.Api.MessagesTest do + use BirdyChatWeb.ConnCase + + setup %{conn: conn} do + url = ~p"/api/messages" + + conn = + conn + |> put_req_header("content-type", "application/json") + + %{conn: conn, url: url} + end + + describe "POST /api/messages" do + test "returns errors for invalid message", %{conn: conn, url: url} do + payload = Jason.encode!(%{}) + conn = post(conn, url, payload) + assert result = json_response(conn, :unprocessable_entity) + + expected_result = %{ + "errors" => %{ + "from" => ["can't be blank"], + "message" => ["can't be blank"], + "to" => ["can't be blank"] + } + } + + assert result == expected_result + end + + test "returns message and ok when successful", %{conn: conn, url: url} do + message = %{"from" => "2-user", "to" => "1-user", "message" => "123"} + payload = Jason.encode!(message) + conn = post(conn, url, payload) + assert result = json_response(conn, :created) + + assert result == message + end + end +end