From e5e0ec4d17c7436ab72554c96c527fb837c1ad71 Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Mon, 14 Mar 2022 18:06:01 -0700 Subject: [PATCH 1/6] Fix Version Change Merge Conflicts --- lib/todo.ex | 24 ++++++++++++---- lib/todo_txt.ex | 62 ++++++++++++++++++++++++++++++++++++++++++ mix.exs | 1 + test/todo_txt_test.exs | 10 +++++++ 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 lib/todo_txt.ex create mode 100644 test/todo_txt_test.exs diff --git a/lib/todo.ex b/lib/todo.ex index fe6b56e..927ab7f 100644 --- a/lib/todo.ex +++ b/lib/todo.ex @@ -147,14 +147,24 @@ defmodule Todo do iex> Todo.parse("task meta:data meta1:data1") %Todo{description: "task", additional_fields: %{"meta" => "data", "meta1" => "data1"}} + iex> Todo.parse("task meta:data meta1:data1 @context") + %Todo{description: "task @context", contexts: [:context], additional_fields: %{"meta" => "data", "meta1" => "data1"}} + iex> Todo.parse("task due:2021-09-13 meta:data meta1:data1") %Todo{description: "task", additional_fields: %{"meta" => "data", "meta1" => "data1"}, due_date: ~D[2021-09-13]} """ def parse(str) do case parser(str) do - {:ok, parsed, "", _, _, _} -> Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) - {:error, message, _, _, _, _} -> message + {:ok, parsed, "", _, _, _} -> + Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) + + {:ok, parsed, additional_description, _, _, _} -> + todo = Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) + set_from_parsed({:description, additional_description}, todo) + + {:error, message, _, _, _, _} -> + message end end @@ -294,6 +304,8 @@ defmodule Todo do end defp set_from_parsed({:description, description}, todo) do + %Todo{description: d, contexts: c, projects: p} = todo + contexts = case context_parser(description) do {:ok, contexts, _, _, _, _} -> contexts @@ -306,9 +318,11 @@ defmodule Todo do {:error, message, _, _, _, _} -> {:error, message} end - Map.put(todo, :description, description) - |> Map.put(:contexts, contexts) - |> Map.put(:projects, projects) + new_descriptions = String.trim(d <> description) + + Map.put(todo, :description, new_descriptions) + |> Map.put(:contexts, c ++ contexts) + |> Map.put(:projects, p ++ projects) end defp set_from_parsed({:done, done}, todo) do diff --git a/lib/todo_txt.ex b/lib/todo_txt.ex new file mode 100644 index 0000000..eb7671f --- /dev/null +++ b/lib/todo_txt.ex @@ -0,0 +1,62 @@ +defmodule TodoTxt do + @moduledoc """ + GenServer that allows interaction with a Todo.txt and optionally a Done.txt file + """ + defmodule State do + @enforce_keys [:todo_txt_file_path] + + defstruct todo_txt_file_path: :none, + done_txt_file_path: :none, + options: [], + todos: [], + history: :none + + def new(input), do: load_todos(struct(State, input)) + + defp validate(state) do + %State{todo_txt_file_path: todo_txt_file_path, done_txt_file_path: done_txt_file_path} = + state + + cond do + !File.exists?(todo_txt_file_path) -> + {:error, "File #{todo_txt_file_path} does not exist"} + + done_txt_file_path != :none && !File.exists?(done_txt_file_path) -> + {:error, "File #{done_txt_file_path} does not exist"} + + true -> + {:ok, state} + end + end + + def load_todos(state) do + {:ok, %State{todo_txt_file_path: todo_txt_file_path}} = validate(state) + + todos = + todo_txt_file_path + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.map(&Todo.parse/1) + + Map.put(state, :todos, todos) + end + end + + use GenServer + + def start_link(args), do: GenServer.start_link(__MODULE__, State.new(args), name: __MODULE__) + + def todos do + GenServer.call(__MODULE__, :todos) + end + + @impl true + def init(state), do: {:ok, state} + # TODO: Add file watcher + + @impl true + def handle_call(:todos, _from, %State{} = state) do + reloaded_state = State.load_todos(state) + {:reply, Map.get(reloaded_state, :todos), reloaded_state} + end +end diff --git a/mix.exs b/mix.exs index 8106495..6f50516 100644 --- a/mix.exs +++ b/mix.exs @@ -61,6 +61,7 @@ defmodule TodoTxt.MixProject do {:dialyxir, "~> 1.0.0", only: :dev, runtime: false}, {:ex_doc, "~> 0.23.0", only: :dev, runtime: false}, {:excoveralls, "~> 0.13.0", only: :test}, + {:file_system, "~> 0.2"}, {:git_hooks, "~> 0.5.2", only: :dev, runtime: false}, {:mix_test_watch, "~> 1.0.2", only: :dev, runtime: false}, {:nimble_parsec, "~> 1.1.0"}, diff --git a/test/todo_txt_test.exs b/test/todo_txt_test.exs new file mode 100644 index 0000000..6479154 --- /dev/null +++ b/test/todo_txt_test.exs @@ -0,0 +1,10 @@ +defmodule TodoTxtTest do + use ExUnit.Case + doctest TodoTxt + + test "init" do + # TODO write tests + # - Test :os.type + assert true + end +end From 887df62a162cafc946e485c0b7162c428b4c6349 Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Mon, 14 Mar 2022 18:27:03 -0700 Subject: [PATCH 2/6] Create todo.txt Fixture --- test/fixtures/todo_txts/todo.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test/fixtures/todo_txts/todo.txt diff --git a/test/fixtures/todo_txts/todo.txt b/test/fixtures/todo_txts/todo.txt new file mode 100644 index 0000000..f4e4754 --- /dev/null +++ b/test/fixtures/todo_txts/todo.txt @@ -0,0 +1,8 @@ +(A) Call Mom @Phone +Family due:2022-02-01 +2022-03-14 (A) Schedule annual checkup +Health +(B) Outline chapter 5 +Novel @Computer (#pomo: 4/20) +(C) Add cover sheets @Office +TPSReports +Plan backyard herb garden @Home +Pick up milk @GroceryStore +Research self-publishing services +Novel @Computer +x Download Todo.txt mobile app @Phone From ba57207903d411305b1776b623e0d889c3e183fc Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Mon, 14 Mar 2022 18:28:36 -0700 Subject: [PATCH 3/6] Add TODO to TodoTxt GenServer Test --- test/todo_txt_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/todo_txt_test.exs b/test/todo_txt_test.exs index 6479154..8d072d9 100644 --- a/test/todo_txt_test.exs +++ b/test/todo_txt_test.exs @@ -3,8 +3,8 @@ defmodule TodoTxtTest do doctest TodoTxt test "init" do - # TODO write tests - # - Test :os.type + # TODO programatically get path for ./fixtures/todo_txts/todo.txt + # Test that gen server starts when path is passed to start_link assert true end end From a56cd30f070b73cc433b3d1c9f4708d7827c44a2 Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Wed, 18 May 2022 22:53:29 -0700 Subject: [PATCH 4/6] Update todo_txt --- .tool-versions | 2 ++ README.md | 1 + lib/todo_txt.ex | 62 ------------------------------------- lib/todo_txt/application.ex | 20 ++++++++++++ lib/todo_txt/server.ex | 25 +++++++++++++++ lib/todo_txt/state.ex | 51 ++++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 62 deletions(-) create mode 100644 .tool-versions delete mode 100644 lib/todo_txt.ex create mode 100644 lib/todo_txt/application.ex create mode 100644 lib/todo_txt/server.ex create mode 100644 lib/todo_txt/state.ex diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..a7903fe --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.13.2-otp-24 +erlang 24.0.5 diff --git a/README.md b/README.md index d0a951f..4e6a4fe 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,4 @@ File.write!("done.txt", Enum.join(Enum.map(done, &Todo.to_string/1), "\n")) ## Roadmap - Add a File Watcher for Local todo.txt - Add a File Watcher for Google Drive todo.txt +- Add Vapor for starting application with config file or environment variable diff --git a/lib/todo_txt.ex b/lib/todo_txt.ex deleted file mode 100644 index eb7671f..0000000 --- a/lib/todo_txt.ex +++ /dev/null @@ -1,62 +0,0 @@ -defmodule TodoTxt do - @moduledoc """ - GenServer that allows interaction with a Todo.txt and optionally a Done.txt file - """ - defmodule State do - @enforce_keys [:todo_txt_file_path] - - defstruct todo_txt_file_path: :none, - done_txt_file_path: :none, - options: [], - todos: [], - history: :none - - def new(input), do: load_todos(struct(State, input)) - - defp validate(state) do - %State{todo_txt_file_path: todo_txt_file_path, done_txt_file_path: done_txt_file_path} = - state - - cond do - !File.exists?(todo_txt_file_path) -> - {:error, "File #{todo_txt_file_path} does not exist"} - - done_txt_file_path != :none && !File.exists?(done_txt_file_path) -> - {:error, "File #{done_txt_file_path} does not exist"} - - true -> - {:ok, state} - end - end - - def load_todos(state) do - {:ok, %State{todo_txt_file_path: todo_txt_file_path}} = validate(state) - - todos = - todo_txt_file_path - |> File.read!() - |> String.split("\n", trim: true) - |> Enum.map(&Todo.parse/1) - - Map.put(state, :todos, todos) - end - end - - use GenServer - - def start_link(args), do: GenServer.start_link(__MODULE__, State.new(args), name: __MODULE__) - - def todos do - GenServer.call(__MODULE__, :todos) - end - - @impl true - def init(state), do: {:ok, state} - # TODO: Add file watcher - - @impl true - def handle_call(:todos, _from, %State{} = state) do - reloaded_state = State.load_todos(state) - {:reply, Map.get(reloaded_state, :todos), reloaded_state} - end -end diff --git a/lib/todo_txt/application.ex b/lib/todo_txt/application.ex new file mode 100644 index 0000000..0ffb3b3 --- /dev/null +++ b/lib/todo_txt/application.ex @@ -0,0 +1,20 @@ +defmodule TodoTxt.Application do + @moduledoc false + + use Application + + def start(_type, args) do + # Set default paths if none are passed + todo_txt_file_path = get_in(args, [Access.key(:todo_txt_file_path, "$TODO_DIR/todo.txt")]) + done_txt_file_path = get_in(args, [Access.key(:done_txt_file_path, "$TODO_DIR/done.txt")]) + + children = [ + {TodoTxt.Server, + todo_txt_file_path: todo_txt_file_path, done_txt_file_path: done_txt_file_path} + # TODO: Add file_system watcher here + ] + + opts = [strategy: :one_for_one, name: VaporExample.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/todo_txt/server.ex b/lib/todo_txt/server.ex new file mode 100644 index 0000000..9f56832 --- /dev/null +++ b/lib/todo_txt/server.ex @@ -0,0 +1,25 @@ +defmodule TodoTxt.Server do + @moduledoc """ + GenServer that allows interaction with a Todo.txt and optionally a Done.txt file + """ + + use GenServer + + alias TodoTxt.State + + def start_link(args), do: GenServer.start_link(__MODULE__, State.new(args), name: __MODULE__) + + def todos do + GenServer.call(__MODULE__, :todos) + end + + @impl true + def init(state), do: {:ok, state} + # TODO: Add file watcher + + @impl true + def handle_call(:todos, _from, %State{} = state) do + reloaded_state = State.load_todos(state) + {:reply, Map.get(reloaded_state, :todos), reloaded_state} + end +end diff --git a/lib/todo_txt/state.ex b/lib/todo_txt/state.ex new file mode 100644 index 0000000..41048b9 --- /dev/null +++ b/lib/todo_txt/state.ex @@ -0,0 +1,51 @@ +defmodule TodoTxt.State do + @enforce_keys [:todo_txt_file_path] + + alias TodoTxt.State + + defstruct todo_txt_file_path: :none, + done_txt_file_path: :none, + file_location: :local, + options: [], + todos: [], + history: :none + + def new(input), do: load_todos(struct(State, input)) + + defp validate(state) do + %State{ + todo_txt_file_path: todo_txt_file_path, + done_txt_file_path: done_txt_file_path, + file_location: file_location + } = state + + case file_location do + :local -> + cond do + !File.exists?(todo_txt_file_path) -> + {:error, "File #{todo_txt_file_path} does not exist"} + + done_txt_file_path != :none && !File.exists?(done_txt_file_path) -> + {:error, "File #{done_txt_file_path} does not exist"} + + true -> + {:ok, state} + end + + invalid_location -> + {:error, "#{invalid_location} is not a valid file_location"} + end + end + + def load_todos(state) do + {:ok, %State{todo_txt_file_path: todo_txt_file_path}} = validate(state) + + todos = + todo_txt_file_path + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.map(&Todo.parse/1) + + Map.put(state, :todos, todos) + end +end From 934c2d2b3cf82f6cf0aea0d02c7885baf8cdcfc6 Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Thu, 18 Aug 2022 00:42:43 -0700 Subject: [PATCH 5/6] Start Tests For TodoTxt.Server --- lib/todo_txt/application.ex | 1 + lib/todo_txt/state.ex | 10 +++++----- test/fixtures/todo_txts/todo.txt | 2 +- test/todo_txt_test.exs | 10 ---------- 4 files changed, 7 insertions(+), 16 deletions(-) delete mode 100644 test/todo_txt_test.exs diff --git a/lib/todo_txt/application.ex b/lib/todo_txt/application.ex index 0ffb3b3..53e037b 100644 --- a/lib/todo_txt/application.ex +++ b/lib/todo_txt/application.ex @@ -4,6 +4,7 @@ defmodule TodoTxt.Application do use Application def start(_type, args) do + # TODO: figure this out # Set default paths if none are passed todo_txt_file_path = get_in(args, [Access.key(:todo_txt_file_path, "$TODO_DIR/todo.txt")]) done_txt_file_path = get_in(args, [Access.key(:done_txt_file_path, "$TODO_DIR/done.txt")]) diff --git a/lib/todo_txt/state.ex b/lib/todo_txt/state.ex index 41048b9..36bb534 100644 --- a/lib/todo_txt/state.ex +++ b/lib/todo_txt/state.ex @@ -1,16 +1,16 @@ defmodule TodoTxt.State do - @enforce_keys [:todo_txt_file_path] - alias TodoTxt.State - defstruct todo_txt_file_path: :none, - done_txt_file_path: :none, + defstruct todo_txt_file_path: "#{System.get_env("TODO_DIR")}/todo.txt", + done_txt_file_path: "#{System.get_env("TODO_DIR")}/done.txt", file_location: :local, options: [], todos: [], history: :none - def new(input), do: load_todos(struct(State, input)) + def new(state = %State{}) do + load_todos(state) + end defp validate(state) do %State{ diff --git a/test/fixtures/todo_txts/todo.txt b/test/fixtures/todo_txts/todo.txt index f4e4754..c6f4b46 100644 --- a/test/fixtures/todo_txts/todo.txt +++ b/test/fixtures/todo_txts/todo.txt @@ -4,5 +4,5 @@ (C) Add cover sheets @Office +TPSReports Plan backyard herb garden @Home Pick up milk @GroceryStore -Research self-publishing services +Novel @Computer +Research self-publishing services +Novel @Computer url:https://www.selfpublishingservices.com x Download Todo.txt mobile app @Phone diff --git a/test/todo_txt_test.exs b/test/todo_txt_test.exs deleted file mode 100644 index 8d072d9..0000000 --- a/test/todo_txt_test.exs +++ /dev/null @@ -1,10 +0,0 @@ -defmodule TodoTxtTest do - use ExUnit.Case - doctest TodoTxt - - test "init" do - # TODO programatically get path for ./fixtures/todo_txts/todo.txt - # Test that gen server starts when path is passed to start_link - assert true - end -end From 0b264e8c482bfd9b465a3ce046838404b95fad92 Mon Sep 17 00:00:00 2001 From: Kyle San Clemente Date: Thu, 18 Aug 2022 00:43:10 -0700 Subject: [PATCH 6/6] Add done.txt Fixture and Server Test --- test/fixtures/todo_txts/done.txt | 4 ++++ test/todo_txt/server_test.exs | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 test/fixtures/todo_txts/done.txt create mode 100644 test/todo_txt/server_test.exs diff --git a/test/fixtures/todo_txts/done.txt b/test/fixtures/todo_txts/done.txt new file mode 100644 index 0000000..b7d5a54 --- /dev/null +++ b/test/fixtures/todo_txts/done.txt @@ -0,0 +1,4 @@ +x 2021-05-11 Print out National Park Permit +SanJacintoTrip +x 2022-07-20 2022-07-19 Flush water heater @home pri:A +x 2022-06-27 2022-06-27 go through @work email +x 2022-06-27 2022-04-15 Organize ideas section of Proposals @work diff --git a/test/todo_txt/server_test.exs b/test/todo_txt/server_test.exs new file mode 100644 index 0000000..92f8d52 --- /dev/null +++ b/test/todo_txt/server_test.exs @@ -0,0 +1,10 @@ +defmodule TodoTxt.ServerTest do + use ExUnit.Case + doctest TodoTxt.Server + + describe "start_link/1" do + test "accepts a TodoTxt.State on start" do + assert {:ok, _pid} = TodoTxt.Server.start_link(%TodoTxt.State{}) + end + end +end