Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Agent Journal

exercises/agent_journal.livemd

Agent Journal

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue State: Agent And ETSETS Inventory Management

Agent Journal

For this exercise, you’re going to create a Journal Agent which will store journal entries as strings.

Example Solution

defmodule Journal do
  use Agent

  def start_link(entries) do
    Agent.start_link(fn -> entries end)
  end

  def all_entries(pid, opts \\ []) do
    entries = Agent.get(pid, fn state -> state end)
    if opts[:order] == :desc, do: Enum.reverse(entries), else: entries
  end

  def add_entry(pid, entry) do
    # Using ++ is less performant than prepending.
    # A more optimal solution would be to store entries in reverse order so they can be prepended.
    Agent.update(pid, fn state -> state ++ [entry] end)
  end
end

Implement the Journal module as documented below.

defmodule Journal do
  @moduledoc """
  Documentation for `Journal`
  """
  use Agent

  @doc """
  Start the Agent process.

  ## Examples

      Default.

      iex> {:ok, pid} = Journal.start_link([])

      With initial entries.

      iex> {:ok, pid} = Journal.start_link(["Entry1", "Entry 2"])
  """
  def start_link(_opts) do
  end

  @doc """
  Get all journal entries.

  ## Examples
      
      Empty journal.
      
      iex> {:ok, pid} = Journal.start_link([])
      iex> Journal.all_entries(pid)
      []

      Journal with entries. Entries are returned in ascending order (oldest entries first).

      iex> {:ok, pid} = Journal.start_link(["Entry 1", "Entry 2"])
      iex> Journal.all_entries(pid)
      ["Entry 1", "Entry 2"]
       
      Ascending order (default).
      
      iex> {:ok, pid} = Journal.start_link(["Entry 1", "Entry 2"])
      iex> Journal.all_entries(pid, order: :asc)
      ["Entry 1", "Entry 2"]

      Descending order.

      iex> {:ok, pid} = Journal.start_link(["Entry 1", "Entry 2"])
      iex> Journal.all_entries(pid, order: :desc)
      ["Entry 2", "Entry 1"]
  """
  def all_entries(pid, opts \\ []) do
  end

  @doc """
  Add a journal entry.

  ## Examples

    iex> {:ok, pid} = Journal.start_link([])
    iex> Journal.add_entry(pid, "Entry 1")
    :ok
    iex> Journal.add_entry(pid, "Entry 2")
    :ok
    iex> Journal.all_entries(pid)
    ["Entry 1", "Entry 2"]
  """
  def add_entry(pid, entry) do
  end
end
:

Bonus: Advanced Journal

Expand upon your original journal with several additional features. Journal entries will now be stored as a struct with :id, :title, :content, :updated_at, and created_at fields. Enforce all keys.

example_entry = %AdvancedJournal{
  # ids start at `0` and auto increment with each new entry.
  id: 0,
  title: "Title",
  content: "Content",
  created_at: DateTime.utc_now(),
  updated_at: DateTime.utc_now()
}

Example Solution

defmodule AdvancedJournal do
  use Agent

  @enforce_keys [:id, :title, :content, :created_at, :updated_at]
  defstruct @enforce_keys

  def start_link(entry_attrs) do
    entries =
      entry_attrs
      |> Enum.with_index()
      |> Enum.map(fn {attrs, index} -> make_entry(index, attrs) end)
      |> Enum.reverse()

    Agent.start_link(fn -> %{entries: entries, current_index: Enum.count(entries)} end)
  end

  def all_entries(pid, opts \\ []) do
    entries = Agent.get(pid, fn state -> state.entries end)
    if opts[:order] == :desc, do: entries, else: Enum.reverse(entries)
  end

  def add_entry(pid, attrs) do
    Agent.update(pid, fn state ->
      entry = make_entry(state.current_index, attrs)

      %{
        state
        | current_index: state.current_index + 1,
          entries: [entry | state.entries]
      }
    end)
  end

  def update_entry(pid, id, attrs) do
    Agent.update(pid, fn state ->
      index = Enum.find_index(state.entries, fn entry -> entry.id == id end)

      new_entries = List.update_at(state.entries, index, fn entry -> Map.merge(entry, attrs) end)

      %{state | entries: new_entries}
    end)
  end

  def delete_entry(pid, id) do
    Agent.update(pid, fn state ->
      new_entries = Enum.reject(state.entries, fn entry -> entry.id == id end)
      %{state | entries: new_entries}
    end)
  end

  def make_entry(index, attrs) do
    %__MODULE__{
      id: index,
      title: attrs[:title] || "",
      content: attrs[:content] || "",
      created_at: DateTime.utc_now(),
      updated_at: DateTime.utc_now()
    }
  end
end

Implement the AdvancedJournal using Agent as documented below.

defmodule AdvancedJournal do
  @moduledoc """
  Documentation for `AdvancedJournal`
  """
  use Agent

  @doc """
  Start the Agent process

  ## Examples

      Default.

      iex> {:ok, pid} = AdvancedJournal.start_link([])

      With initial entries.

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Entry 1", content: "Entry 1 Content"}])
  """
  def start_link(opts) do
  end

  @doc """
  Return all entries. Entries are automatically assigned an `:id` starting at `0` in the
  order they are created in. The `:created_at` and `:updated_at` fields should be the current
  UTC [DateTime](https://hexdocs.pm/elixir/DateTime.html) at the moment of creation.

  We do not include `:created_at` and `:updated_at` in doctests as small differences 
  in time could cause these tests to fail.

  ## Examples

      Empty journal.

      iex> {:ok, pid} = AdvancedJournal.start_link([])
      iex> AdvancedJournal.all_entries(pid)
      []

      One entry. 

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Title", content: "Content"}])
      iex> [%AdvancedJournal{id: 0, title: "Title", content: "Content", created_at: _, updated_at: _}] = AdvancedJournal.all_entries(pid)

      Multiple entries. Order is ascending by default.

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Entry 1", content: "Entry 1"}, %{title: "Entry 2", content: "Entry 2"}])
      iex>
      ..>[
      ..>  %AdvancedJournal{id: 0, title: "Entry 1", content: "Entry 1", created_at: _, updated_at: _},
      ..>  %AdvancedJournal{id: 1, title: "Entry 2", content: "Entry 2", created_at: _, updated_at: _}
      ..>] = AdvancedJournal.all_entries(pid)

      Ascending order (default).

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Entry 1", content: "Entry 1"}, %{title: "Entry 2", content: "Entry 2"}])
      iex>
      ..>[
      ..>  %AdvancedJournal{id: 0, title: "Entry 1", content: "Entry 1", created_at: _, updated_at: _},
      ..>  %AdvancedJournal{id: 1, title: "Entry 2", content: "Entry 2", created_at: _, updated_at: _}
      ..>] = AdvancedJournal.all_entries(pid, order: :asc)

      Descending order.

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Entry 1", content: "Entry 1"}, %{title: "Entry 2", content: "Entry 2"}])
      iex>
      ..>[
      ..>  %AdvancedJournal{id: 1, title: "Entry 2", content: "Entry 2", created_at: _, updated_at: _}
      ..>  %AdvancedJournal{id: 0, title: "Entry 1", content: "Entry 1", created_at: _, updated_at: _},
      ..>] = AdvancedJournal.all_entries(pid, order: :desc)
  """
  def all_entries(pid, opts \\ []) do
  end

  @doc """
  Add a journal entry. Automatically create `:id`, `:created_at`, and `:updated_at` fields.

  ## Examples

      One entry.

      iex> {:ok, pid} = AdvancedJournal.start_link([])
      iex> AdvancedJournal.add_entry(pid, %{title: "Title", content: "Content"})
      iex> [%AdvancedJournal{id: 0, title: "Title", content: "Content", created_at: _, updated_at: _}] = AdvancedJournal.all_entries(pid)

      Multiple entries.

      iex> {:ok, pid} = AdvancedJournal.start_link([])
      iex> AdvancedJournal.add_entry(pid, %{title: "Entry 1", content: "Entry 1"})
      iex> AdvancedJournal.add_entry(pid, %{title: "Entry 2", content: "Entry 2"})
      iex>
      ..>[
      ..>  %AdvancedJournal{id: 0, title: "Entry 1", content: "Entry 1", created_at: _, updated_at: _}
      ..>  %AdvancedJournal{id: 1, title: "Entry 2", content: "Entry 2", created_at: _, updated_at: _},
      ..>] = AdvancedJournal.all_entries(pid)
  """
  def add_entry(pid, attrs) do
  end

  @doc """
  Update a journal entry. 
  This should automatically set the `:updated_at` field to the current [DateTime](https://hexdocs.pm/elixir/DateTime.html).

  ## Examples

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Title", content: "Content"}])
      iex> AdvancedJournal.update_entry(pid, 0, %{title: "Updated Title", content: "Updated Content"})
      iex> [%AdvancedJournal{id: 0, title: "Updated Title", content: "Updated Content", created_at: _, updated_at: _}] = AdvancedJournal.all_entries(pid)
  """
  def update_entry(pid, id, attrs) do
  end

  @doc """
  Delete a journal entry by it's id.

  ## Examples

      iex> {:ok, pid} = AdvancedJournal.start_link([%{title: "Title", content: "Content"}])
      iex> AdvancedJournal.delete_entry(pid, 0)
      iex> []
  """
  def delete_entry(pid, id, attrs) do
  end
end

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Agent Journal exercise"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue State: Agent And ETSETS Inventory Management