Public JSON API for items

This commit is contained in:
2026-01-25 09:09:10 +01:00
parent 4fd9eab52b
commit d049886e9d
11 changed files with 459 additions and 3 deletions

View File

@ -0,0 +1,104 @@
defmodule GenericRestServer.Items do
@moduledoc """
The Items context.
"""
import Ecto.Query, warn: false
alias GenericRestServer.Repo
alias GenericRestServer.Items.Item
@doc """
Returns the list of items.
## Examples
iex> list_items()
[%Item{}, ...]
"""
def list_items do
Repo.all(Item)
end
@doc """
Gets a single item.
Raises `Ecto.NoResultsError` if the Item does not exist.
## Examples
iex> get_item!(123)
%Item{}
iex> get_item!(456)
** (Ecto.NoResultsError)
"""
def get_item!(id), do: Repo.get!(Item, id)
@doc """
Creates a item.
## Examples
iex> create_item(%{field: value})
{:ok, %Item{}}
iex> create_item(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_item(attrs) do
%Item{}
|> Item.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a item.
## Examples
iex> update_item(item, %{field: new_value})
{:ok, %Item{}}
iex> update_item(item, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_item(%Item{} = item, attrs) do
item
|> Item.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a item.
## Examples
iex> delete_item(item)
{:ok, %Item{}}
iex> delete_item(item)
{:error, %Ecto.Changeset{}}
"""
def delete_item(%Item{} = item) do
Repo.delete(item)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking item changes.
## Examples
iex> change_item(item)
%Ecto.Changeset{data: %Item{}}
"""
def change_item(%Item{} = item, attrs \\ %{}) do
Item.changeset(item, attrs)
end
end

View File

@ -0,0 +1,23 @@
defmodule GenericRestServer.Items.Item do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "items" do
field :name, :string
field :description, :string
field :info, :string
field :amount, :integer
field :factor, :float
timestamps(type: :utc_datetime)
end
@doc false
def changeset(item, attrs) do
item
|> cast(attrs, [:name, :description, :info, :amount, :factor])
|> validate_required([:name, :description, :info, :amount, :factor])
end
end

View File

@ -0,0 +1,25 @@
defmodule GenericRestServerWeb.ChangesetJSON do
@doc """
Renders changeset errors.
"""
def error(%{changeset: changeset}) do
# When encoded, the changeset returns its errors
# as a JSON object. So we just pass it forward.
%{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
end
defp translate_error({msg, opts}) do
# You can make use of gettext to translate error messages by
# uncommenting and adjusting the following code:
# if count = opts[:count] do
# Gettext.dngettext(GenericRestServerWeb.Gettext, "errors", msg, msg, count, opts)
# else
# Gettext.dgettext(GenericRestServerWeb.Gettext, "errors", msg, opts)
# end
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
end

View File

@ -0,0 +1,24 @@
defmodule GenericRestServerWeb.FallbackController do
@moduledoc """
Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details.
"""
use GenericRestServerWeb, :controller
# This clause handles errors returned by Ecto's insert/update/delete.
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: GenericRestServerWeb.ChangesetJSON)
|> render(:error, changeset: changeset)
end
# This clause is an example of how to handle resources that cannot be found.
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(html: GenericRestServerWeb.ErrorHTML, json: GenericRestServerWeb.ErrorJSON)
|> render(:"404")
end
end

View File

@ -0,0 +1,43 @@
defmodule GenericRestServerWeb.ItemController do
use GenericRestServerWeb, :controller
alias GenericRestServer.Items
alias GenericRestServer.Items.Item
action_fallback GenericRestServerWeb.FallbackController
def index(conn, _params) do
items = Items.list_items()
render(conn, :index, items: items)
end
def create(conn, %{"item" => item_params}) do
with {:ok, %Item{} = item} <- Items.create_item(item_params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/api/items/#{item}")
|> render(:show, item: item)
end
end
def show(conn, %{"id" => id}) do
item = Items.get_item!(id)
render(conn, :show, item: item)
end
def update(conn, %{"id" => id, "item" => item_params}) do
item = Items.get_item!(id)
with {:ok, %Item{} = item} <- Items.update_item(item, item_params) do
render(conn, :show, item: item)
end
end
def delete(conn, %{"id" => id}) do
item = Items.get_item!(id)
with {:ok, %Item{}} <- Items.delete_item(item) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -0,0 +1,28 @@
defmodule GenericRestServerWeb.ItemJSON do
alias GenericRestServer.Items.Item
@doc """
Renders a list of items.
"""
def index(%{items: items}) do
%{data: for(item <- items, do: data(item))}
end
@doc """
Renders a single item.
"""
def show(%{item: item}) do
%{data: data(item)}
end
defp data(%Item{} = item) do
%{
id: item.id,
name: item.name,
description: item.description,
info: item.info,
amount: item.amount,
factor: item.factor
}
end
end

View File

@ -21,9 +21,11 @@ defmodule GenericRestServerWeb.Router do
end
# Other scopes may use custom stacks.
# scope "/api", GenericRestServerWeb do
# pipe_through :api
# end
scope "/api", GenericRestServerWeb do
pipe_through :api
resources "/items", ItemController, except: [:new, :edit]
end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:generic_rest_server, :dev_routes) do

View File

@ -0,0 +1,16 @@
defmodule GenericRestServer.Repo.Migrations.CreateItems do
use Ecto.Migration
def change do
create table(:items, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string
add :description, :string
add :info, :string
add :amount, :integer
add :factor, :float
timestamps(type: :utc_datetime)
end
end
end

View File

@ -0,0 +1,67 @@
defmodule GenericRestServer.ItemsTest do
use GenericRestServer.DataCase
alias GenericRestServer.Items
describe "items" do
alias GenericRestServer.Items.Item
import GenericRestServer.ItemsFixtures
@invalid_attrs %{info: nil, name: nil, description: nil, amount: nil, factor: nil}
test "list_items/0 returns all items" do
item = item_fixture()
assert Items.list_items() == [item]
end
test "get_item!/1 returns the item with given id" do
item = item_fixture()
assert Items.get_item!(item.id) == item
end
test "create_item/1 with valid data creates a item" do
valid_attrs = %{info: "some info", name: "some name", description: "some description", amount: 42, factor: 120.5}
assert {:ok, %Item{} = item} = Items.create_item(valid_attrs)
assert item.info == "some info"
assert item.name == "some name"
assert item.description == "some description"
assert item.amount == 42
assert item.factor == 120.5
end
test "create_item/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Items.create_item(@invalid_attrs)
end
test "update_item/2 with valid data updates the item" do
item = item_fixture()
update_attrs = %{info: "some updated info", name: "some updated name", description: "some updated description", amount: 43, factor: 456.7}
assert {:ok, %Item{} = item} = Items.update_item(item, update_attrs)
assert item.info == "some updated info"
assert item.name == "some updated name"
assert item.description == "some updated description"
assert item.amount == 43
assert item.factor == 456.7
end
test "update_item/2 with invalid data returns error changeset" do
item = item_fixture()
assert {:error, %Ecto.Changeset{}} = Items.update_item(item, @invalid_attrs)
assert item == Items.get_item!(item.id)
end
test "delete_item/1 deletes the item" do
item = item_fixture()
assert {:ok, %Item{}} = Items.delete_item(item)
assert_raise Ecto.NoResultsError, fn -> Items.get_item!(item.id) end
end
test "change_item/1 returns a item changeset" do
item = item_fixture()
assert %Ecto.Changeset{} = Items.change_item(item)
end
end
end

View File

@ -0,0 +1,100 @@
defmodule GenericRestServerWeb.ItemControllerTest do
use GenericRestServerWeb.ConnCase
import GenericRestServer.ItemsFixtures
alias GenericRestServer.Items.Item
@create_attrs %{
info: "some info",
name: "some name",
description: "some description",
amount: 42,
factor: 120.5
}
@update_attrs %{
info: "some updated info",
name: "some updated name",
description: "some updated description",
amount: 43,
factor: 456.7
}
@invalid_attrs %{info: nil, name: nil, description: nil, amount: nil, factor: nil}
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "lists all items", %{conn: conn} do
conn = get(conn, ~p"/api/items")
assert json_response(conn, 200)["data"] == []
end
end
describe "create item" do
test "renders item when data is valid", %{conn: conn} do
conn = post(conn, ~p"/api/items", item: @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, ~p"/api/items/#{id}")
assert %{
"id" => ^id,
"amount" => 42,
"description" => "some description",
"factor" => 120.5,
"info" => "some info",
"name" => "some name"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/api/items", item: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "update item" do
setup [:create_item]
test "renders item when data is valid", %{conn: conn, item: %Item{id: id} = item} do
conn = put(conn, ~p"/api/items/#{item}", item: @update_attrs)
assert %{"id" => ^id} = json_response(conn, 200)["data"]
conn = get(conn, ~p"/api/items/#{id}")
assert %{
"id" => ^id,
"amount" => 43,
"description" => "some updated description",
"factor" => 456.7,
"info" => "some updated info",
"name" => "some updated name"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn, item: item} do
conn = put(conn, ~p"/api/items/#{item}", item: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "delete item" do
setup [:create_item]
test "deletes chosen item", %{conn: conn, item: item} do
conn = delete(conn, ~p"/api/items/#{item}")
assert response(conn, 204)
assert_error_sent 404, fn ->
get(conn, ~p"/api/items/#{item}")
end
end
end
defp create_item(_) do
item = item_fixture()
%{item: item}
end
end

View File

@ -0,0 +1,24 @@
defmodule GenericRestServer.ItemsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `GenericRestServer.Items` context.
"""
@doc """
Generate a item.
"""
def item_fixture(attrs \\ %{}) do
{:ok, item} =
attrs
|> Enum.into(%{
amount: 42,
description: "some description",
factor: 120.5,
info: "some info",
name: "some name"
})
|> GenericRestServer.Items.create_item()
item
end
end