Preparatory work

This commit is contained in:
Maciej 2026-02-21 13:05:47 +02:00
parent a22ba724df
commit 386a331956
Signed by: maciej
GPG key ID: 28243AF437E32F99
9 changed files with 217 additions and 1 deletions

4
.gitignore vendored
View file

@ -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/

View file

@ -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)

View file

@ -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

46
lib/birdy_chat/message.ex Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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