From 222968c4954bddb56543fcbe16ced1f68ef9bfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 4 Aug 2024 13:21:15 +0200 Subject: [PATCH] feat!: make a project using mix --- .formatter.exs | 4 + README.md | 21 +++ lib/todo/csv_importer.ex | 29 ++++ lib/todo/list.ex | 43 ++++++ lib/todo/server.ex | 67 +++++++++ mix.exs | 28 ++++ test/test_helper.exs | 1 + test/todo/csvimporter_test.exs | 19 +++ .../todo/files/todo_list.csv | 0 test/todo/list_test.exs | 89 +++++++++++ todo_list.ex | 139 ------------------ 11 files changed, 301 insertions(+), 139 deletions(-) create mode 100644 .formatter.exs create mode 100644 README.md create mode 100644 lib/todo/csv_importer.ex create mode 100644 lib/todo/list.ex create mode 100644 lib/todo/server.ex create mode 100644 mix.exs create mode 100644 test/test_helper.exs create mode 100644 test/todo/csvimporter_test.exs rename todo_list.csv => test/todo/files/todo_list.csv (100%) create mode 100644 test/todo/list_test.exs delete mode 100644 todo_list.ex diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..3db1da7 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Todo + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `todo` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:todo, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lib/todo/csv_importer.ex b/lib/todo/csv_importer.ex new file mode 100644 index 0000000..f738225 --- /dev/null +++ b/lib/todo/csv_importer.ex @@ -0,0 +1,29 @@ +defmodule Todo.CSVImporter do + alias Todo.List + + def import(path) do + path + |> File.stream!() + |> Stream.map(&parse_line/1) + |> List.new() + end + + defp parse_line(line) do + line + |> String.trim() + |> String.split(",") + |> create_entry() + end + + defp create_entry([date, title]) do + final_date = parse_date(date) + + %{date: final_date, title: title} + end + + defp parse_date(string) do + [year, month, day] = string |> String.split("-") |> Enum.map(&String.to_integer/1) + + Date.new!(year, month, day) + end +end diff --git a/lib/todo/list.ex b/lib/todo/list.ex new file mode 100644 index 0000000..07c30e3 --- /dev/null +++ b/lib/todo/list.ex @@ -0,0 +1,43 @@ +defmodule Todo.List do + defstruct entries: %{}, next_id: 1 + + def new(entries \\ []) do + Enum.reduce(entries, %__MODULE__{}, &add(&2, &1)) + end + + def add( + %__MODULE__{entries: entries, next_id: next_id} = todo_list, + %{date: _, title: _} = entry + ) do + new_entry = Map.put(entry, :id, next_id) + + new_entries = Map.put(entries, next_id, new_entry) + + %__MODULE__{todo_list | entries: new_entries, next_id: next_id + 1} + end + + def entries(%__MODULE__{} = todo_list, date) do + todo_list.entries + |> Map.filter(fn {_, entry} -> entry.date == date end) + |> Map.values() + end + + def update(%__MODULE__{entries: entries} = todo_list, id, update_fun) + when is_function(update_fun, 1) do + case Map.fetch(entries, id) do + :error -> + todo_list + + {:ok, entry} -> + new_entry = update_fun.(entry) + new_entries = Map.put(entries, id, new_entry) + %__MODULE__{todo_list | entries: new_entries} + end + end + + def delete(%__MODULE__{entries: entries} = todo_list, id) when is_number(id) do + new_entries = Map.delete(entries, id) + + %__MODULE__{todo_list | entries: new_entries} + end +end diff --git a/lib/todo/server.ex b/lib/todo/server.ex new file mode 100644 index 0000000..09e75b7 --- /dev/null +++ b/lib/todo/server.ex @@ -0,0 +1,67 @@ +defmodule Todo.Server do + use GenServer + + @impl GenServer + def init(%_{} = todo_list) do + {:ok, todo_list} + end + + @impl GenServer + def init(entries) do + {:ok, Todo.List.new(entries)} + end + + @impl GenServer + def handle_call({:entries, date}, _from, todo_list) do + entries = Todo.List.entries(todo_list, date) + + {:reply, entries, todo_list} + end + + @impl GenServer + def handle_cast({:add, entry}, todo_list) do + new_todo_list = Todo.List.add(todo_list, entry) + + {:noreply, new_todo_list} + end + + @impl GenServer + def handle_cast({:update, id, update_fun}, todo_list) do + new_todo_list = Todo.List.update(todo_list, id, update_fun) + + {:noreply, new_todo_list} + end + + @impl GenServer + def handle_cast({:delete, id}, todo_list) do + new_todo_list = Todo.List.delete(todo_list, id) + + {:noreply, new_todo_list} + end + + def start(entries \\ []) + + def start(%_{} = todo_list) do + GenServer.start(__MODULE__, todo_list) + end + + def start(entries) do + GenServer.start(__MODULE__, entries) + end + + def add(pid, entry) do + GenServer.cast(pid, {:add, entry}) + end + + def entries(pid, date) do + GenServer.call(pid, {:entries, date}) + end + + def update(pid, id, update_fun) do + GenServer.cast(pid, {:update, id, update_fun}) + end + + def delete(pid, id) do + GenServer.cast(pid, {:delete, id}) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..1ed329f --- /dev/null +++ b/mix.exs @@ -0,0 +1,28 @@ +defmodule Todo.MixProject do + use Mix.Project + + def project do + [ + app: :todo, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test/todo/csvimporter_test.exs b/test/todo/csvimporter_test.exs new file mode 100644 index 0000000..ca06c0a --- /dev/null +++ b/test/todo/csvimporter_test.exs @@ -0,0 +1,19 @@ +defmodule Todo.TestCSVImporter do + use ExUnit.Case + alias Todo.CSVImporter + alias Todo.List + + test "simple correct csv" do + file_path = Path.join(__DIR__, "./files/todo_list.csv") + list = CSVImporter.import(file_path) + + assert [ + %{date: ~D[2024-12-19], title: "Dentist"}, + %{date: ~D[2024-12-19], title: "Movies"} + ] = List.entries(list, ~D[2024-12-19]) + + assert [ + %{date: ~D[2024-12-20], title: "Shopping"} + ] = List.entries(list, ~D[2024-12-20]) + end +end diff --git a/todo_list.csv b/test/todo/files/todo_list.csv similarity index 100% rename from todo_list.csv rename to test/todo/files/todo_list.csv diff --git a/test/todo/list_test.exs b/test/todo/list_test.exs new file mode 100644 index 0000000..b28c0c9 --- /dev/null +++ b/test/todo/list_test.exs @@ -0,0 +1,89 @@ +defmodule Todo.TestList do + use ExUnit.Case + alias Todo.List + + test "simple usage" do + list = List.new() + assert [] = List.entries(list, ~D[2024-08-04]) + + list = List.add(list, %{date: ~D[2024-08-04], title: "Write some Elixir"}) + + assert [%{date: ~D[2024-08-04], title: "Write some Elixir"}] = + List.entries(list, ~D[2024-08-04]) + end + + test "id starts at 1 and increases by 1" do + list = List.new() + list = List.add(list, %{date: ~D[2024-08-04], title: "Write some Elixir"}) + + assert [%{date: ~D[2024-08-04], title: "Write some Elixir", id: 1}] = + List.entries(list, ~D[2024-08-04]) + + list = List.add(list, %{date: ~D[2024-08-04], title: "Write more Elixir"}) + + assert [ + %{date: ~D[2024-08-04], title: "Write some Elixir", id: 1}, + %{date: ~D[2024-08-04], title: "Write more Elixir", id: 2} + ] = + List.entries(list, ~D[2024-08-04]) + end + + test "new/1 has expected elements" do + list = + List.new([ + %{date: ~D[2024-08-04], title: "Write some Elixir"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ]) + + assert [ + %{date: ~D[2024-08-04], title: "Write some Elixir"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ] = + List.entries(list, ~D[2024-08-04]) + end + + test "entries returns correct dates" do + list = + List.new([ + %{date: ~D[2024-08-03], title: "Prepare writing Elixir"}, + %{date: ~D[2024-08-04], title: "Write some Elixir"}, + %{date: ~D[2024-08-05], title: "Pause writing Elixir"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ]) + + assert [%{date: ~D[2024-08-04]}, %{date: ~D[2024-08-04]}] = List.entries(list, ~D[2024-08-04]) + end + + test "update/3 updates selected entry" do + list = + List.new([ + %{date: ~D[2024-08-04], title: "Write some Elixir"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ]) + + updated = + List.update(list, 1, fn %{title: title} = entry -> + %{entry | title: String.upcase(title)} + end) + + assert [ + %{date: ~D[2024-08-04], title: "WRITE SOME ELIXIR"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ] = + List.entries(updated, ~D[2024-08-04]) + end + + test "delete/2" do + list = + List.new([ + %{date: ~D[2024-08-04], title: "Write some Elixir"}, + %{date: ~D[2024-08-04], title: "Write more Elixir"} + ]) + + new_list = List.delete(list, 2) + + assert [ + %{date: ~D[2024-08-04], title: "Write some Elixir"} + ] = List.entries(new_list, ~D[2024-08-04]) + end +end diff --git a/todo_list.ex b/todo_list.ex deleted file mode 100644 index becfb53..0000000 --- a/todo_list.ex +++ /dev/null @@ -1,139 +0,0 @@ -defmodule TodoServer do - use GenServer - - @impl GenServer - def init(%_{} = todo_list) do - {:ok, todo_list} - end - - @impl GenServer - def init(entries) do - {:ok, TodoList.new(entries)} - end - - @impl GenServer - def handle_call({:entries, date}, _from, todo_list) do - entries = TodoList.entries(todo_list, date) - - {:reply, entries, todo_list} - end - - @impl GenServer - def handle_cast({:add, entry}, todo_list) do - new_todo_list = TodoList.add(todo_list, entry) - - {:no_reply, new_todo_list} - end - - @impl GenServer - def handle_cast({:update, id, update_fun}, todo_list) do - new_todo_list = TodoList.update(todo_list, id, update_fun) - - {:no_reply, new_todo_list} - end - - @impl GenServer - def handle_cast({:delete, id}, todo_list) do - new_todo_list = TodoList.delete(todo_list, id) - - {:no_reply, new_todo_list} - end - - def start(entries \\ []) - - def start(%_{} = todo_list) do - GenServer.start(__MODULE__, todo_list) - end - - def start(entries) do - GenServer.start(__MODULE__, entries) - end - - def add(pid, entry) do - GenServer.cast(pid, {:add, entry}) - end - - def entries(pid, date) do - GenServer.call(pid, {:entries, date}) - end - - def update(pid, id, update_fun) do - GenServer.cast(pid, {:update, id, update_fun}) - end - - def delete(pid, id) do - GenServer.cast(pid, {:delete, id}) - end -end - -defmodule TodoList do - defstruct entries: %{}, next_id: 1 - - def new(entries \\ []) do - Enum.reduce(entries, %__MODULE__{}, &add(&2, &1)) - end - - def add( - %__MODULE__{entries: entries, next_id: next_id} = todo_list, - %{date: _, title: _} = entry - ) do - new_entry = Map.put(entry, :id, next_id) - - new_entries = Map.put(entries, next_id, new_entry) - - %__MODULE__{todo_list | entries: new_entries, next_id: next_id + 1} - end - - def entries(%__MODULE__{} = todo_list, date) do - todo_list.entries - |> Map.filter(fn {_, entry} -> entry.date == date end) - |> Map.values() - end - - def update(%__MODULE__{entries: entries} = todo_list, id, update_fun) - when is_function(update_fun, 1) do - case Map.fetch(entries, id) do - :error -> - todo_list - - {:ok, entry} -> - new_entry = update_fun.(entry) - new_entries = Map.put(entries, id, new_entry) - %__MODULE__{todo_list | entries: new_entries} - end - end - - def delete(%__MODULE__{entries: entries} = todo_list, id) when is_number(id) do - new_entries = Map.delete(entries, id) - - %__MODULE__{todo_list | entries: new_entries} - end -end - -defmodule TodoList.CSVImporter do - def import(path) do - path - |> File.stream!() - |> Stream.map(&parse_line/1) - |> TodoList.new() - end - - defp parse_line(line) do - line - |> String.trim() - |> String.split(",") - |> create_entry() - end - - defp create_entry([date, title]) do - final_date = parse_date(date) - - %{date: final_date, title: title} - end - - defp parse_date(string) do - [year, month, day] = string |> String.split("-") |> Enum.map(&String.to_integer/1) - - Date.new!(year, month, day) - end -end