Powered by AppSignal & Oban Pro

Book Changeset

exercises/book_changeset.livemd

Book Changeset

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

Navigation

Return Home Report An Issue

Book Changeset

You’re going to create a Book schemaless changeset struct. A book should have:

  • A required :title field between 3 and 100 characters.
  • A required :content string field.
  • A :published_on date field.
  • A :category field which must be either "action", "fiction", or "mystery" (you may choose to add more categories if you wish)
  • A :books_sold integer which must be 0 or above.
  • A :publisher_email field which must be in the format name@domain.extension
  • An :author string field.
  • An :has_license field which must always be true.
  • A :price integer field which must be above 0.

For example, creating a book with the following invalid fields would return an {:error, changeset} tuple similar to the following.

Book.new(%{category: "invalid", email: "invalid", books_sold: -1, price: -1})
{:error,
 #Ecto.Changeset<
   action: :update,
   changes: %{books_sold: -1, category: "invalid", price: -1},
   errors: [
     price: {"must be greater than %{number}",
      [validation: :number, kind: :greater_than, number: 0]},
     has_license: {"must be accepted", [validation: :acceptance]},
     books_sold: {"must be greater than or equal to %{number}",
      [validation: :number, kind: :greater_than_or_equal_to, number: 0]},
     category: {"is invalid", [validation: :inclusion, enum: ["action", "fiction", "mystery"]]},
     title: {"can't be blank", [validation: :required]},
     content: {"can't be blank", [validation: :required]}
   ],
   data: #Book<>,
   valid?: false
 >}

Hint

Read the Ecto.Changeset documentation. There, you’ll find all of the validate* functions necessary for each field. For example, Ecto.Changeset.validate_change/3 allows you to create custom validation.

You can also refer to the primitive types documentation for the list of allowed field types.

Example Solution

defmodule Book do
  @types %{
    title: :string,
    content: :string,
    published_on: :date,
    category: :string,
    books_sold: :integer,
    publisher_email: :string,
    author: :string,
    has_license: :boolean,
    price: :integer
  }

  @keys Map.keys(@types)
  
  defstruct @keys

  def changeset(%__MODULE__{} = user, params \\ %{}) do
    {user, @types}
    |> Ecto.Changeset.cast(params, @keys)
    |> Ecto.Changeset.validate_required([:title, :content])
    |> Ecto.Changeset.validate_length(:title, min: 3, max: 100)
    |> Ecto.Changeset.validate_inclusion(:category, ["action", "fiction", "mystery"])
    |> Ecto.Changeset.validate_number(:books_sold, greater_than_or_equal_to: 0)
    |> Ecto.Changeset.validate_change(:publisher_email, fn :publisher_email, publisher_email ->
      if Regex.match?(~r/\w+@\w+\.\w+/, publisher_email) do
        []
      else
        [email: "invalid email"]
      end
    end)
    |> Ecto.Changeset.validate_acceptance(:has_license)
    |> Ecto.Changeset.validate_number(:price, greater_than: 0)
  end

  def new(params) do
    %__MODULE__{}
    |> changeset(params)
    |> Ecto.Changeset.apply_action(:update)
  end
end

You should rely on Ecto Changesets and your own custom validation to validate the book information. Enter your solution below.

defmodule Book do
end

Mark As Completed

file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")

save_name =
  case Path.basename(__DIR__) do
    "reading" -> "book_changeset_reading"
    "exercises" -> "book_changeset_exercise"
  end

progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()

default = Map.get(existing_progress, save_name, false)

form =
  Kino.Control.form(
    [
      completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
    ],
    report_changes: true
  )

Task.async(fn ->
  for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
    File.write!(
      progress_path,
      Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
    )
  end
end)

form

Commit Your Progress

Run the following in your command line from the curriculum folder to track and save your progress in a Git commit. Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.

$ git checkout -b book-changeset-exercise
$ git add .
$ git commit -m "finish book changeset exercise"
$ git push origin book-changeset-exercise

Create a pull request from your book-changeset-exercise branch to your solutions branch. Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.

DockYard Academy Students Only:

Notify your teacher by including @BrooklinJazz in your PR description to get feedback. You (or your teacher) may merge your PR into your solutions branch after review.

If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.

Up Next

Previous Next
Sign Up Form Group Project: Blog