From e10498798ef1088e6dc4a0e4606da9064483784d Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Thu, 9 Nov 2023 17:28:08 -0500 Subject: [PATCH] Improve types --- lib/beacon/content/page.ex | 2 +- lib/beacon/types/atom.ex | 15 +++--- lib/beacon/types/binary.ex | 14 +++--- lib/beacon/types/json_array_map.ex | 57 +++++++++++++++++++++++ lib/beacon/types/site.ex | 16 +++---- test/beacon/content_test.exs | 13 ++++++ test/beacon/types/atom_test.exs | 24 ++++++++++ test/beacon/types/binary_test.exs | 23 +++++++++ test/beacon/types/json_array_map_test.exs | 28 +++++++++++ test/beacon/types/site_test.exs | 27 +++++++++++ 10 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 lib/beacon/types/json_array_map.ex create mode 100644 test/beacon/types/atom_test.exs create mode 100644 test/beacon/types/binary_test.exs create mode 100644 test/beacon/types/json_array_map_test.exs create mode 100644 test/beacon/types/site_test.exs diff --git a/lib/beacon/content/page.ex b/lib/beacon/content/page.ex index 3bba2379..56a489a1 100644 --- a/lib/beacon/content/page.ex +++ b/lib/beacon/content/page.ex @@ -36,7 +36,7 @@ defmodule Beacon.Content.Page do field :description, :string field :template, :string field :meta_tags, {:array, :map}, default: [] - field :raw_schema, {:array, :map}, default: [] + field :raw_schema, Beacon.Types.JsonArrayMap, default: [] field :order, :integer, default: 1 field :format, Beacon.Types.Atom, default: :heex field :extra, :map, default: %{} diff --git a/lib/beacon/types/atom.ex b/lib/beacon/types/atom.ex index 0d200489..eaa608ac 100644 --- a/lib/beacon/types/atom.ex +++ b/lib/beacon/types/atom.ex @@ -7,17 +7,16 @@ defmodule Beacon.Types.Atom do def type, do: :atom - def cast(:any, site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} - def cast(:any, site) when is_atom(site), do: {:ok, site} - def cast(:any, _), do: :error - def cast(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} def cast(site) when is_atom(site), do: {:ok, site} - def cast(_), do: :error - - def load(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} + def cast(site), do: {:error, message: "invalid site #{inspect(site)}"} def dump(site) when is_binary(site), do: {:ok, site} def dump(site) when is_atom(site), do: {:ok, Atom.to_string(site)} - def dump(_), do: :error + def dump(_site), do: :error + + def equal?(site1, site2), do: site1 === site2 + + def load(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} + def load(_site), do: :error end diff --git a/lib/beacon/types/binary.ex b/lib/beacon/types/binary.ex index 7bbaeae4..8a00cbf7 100644 --- a/lib/beacon/types/binary.ex +++ b/lib/beacon/types/binary.ex @@ -9,14 +9,12 @@ defmodule Beacon.Types.Binary do def type, do: :binary - def cast(:any, term), do: {:ok, term} - def cast(term), do: {:ok, term} + def cast(term) when is_binary(term), do: {:ok, term} + def cast(term), do: {:ok, :erlang.term_to_binary(term)} - def load(binary) when is_binary(binary) do - {:ok, :erlang.binary_to_term(binary)} - end + def dump(term) when is_binary(term), do: {:ok, term} + def dump(term), do: {:ok, :erlang.term_to_binary(term)} - def dump(term) do - {:ok, :erlang.term_to_binary(term)} - end + def load(binary) when is_binary(binary), do: {:ok, :erlang.binary_to_term(binary)} + def load(_binary), do: :error end diff --git a/lib/beacon/types/json_array_map.ex b/lib/beacon/types/json_array_map.ex new file mode 100644 index 00000000..334bd2c2 --- /dev/null +++ b/lib/beacon/types/json_array_map.ex @@ -0,0 +1,57 @@ +defmodule Beacon.Types.JsonArrayMap do + @moduledoc """ + Convert between json and map enforcing the data shape as array of objects/maps. + """ + + use Ecto.Type + + def type, do: :mine + + def cast(term) when is_map(term), do: {:ok, [term]} + + def cast(term) when is_list(term) do + case validate(term) do + {true, list} -> + {:ok, list} + + {false, list} -> + {:error, message: "expected a list of map or a map, got: #{inspect(list)}"} + end + end + + def cast(term) do + {:error, message: "expected a list of map or a map, got: #{inspect(term)}"} + end + + def dump(term) when is_map(term), do: {:ok, [term]} + + def dump(term) when is_list(term) do + case validate(term) do + {true, list} -> + {:ok, list} + + {false, _list} -> + :error + end + end + + def dump(_site), do: :error + + def load(term) when is_list(term), do: {:ok, term} + def load(_term), do: :error + + defp validate(term) when is_list(term) do + {valid, list} = + Enum.reduce_while(term, {true, []}, fn + t, {_valid, list} when is_map(t) -> + {:cont, {true, [t | list]}} + + t, {_, list} -> + {:halt, {false, [t | list]}} + end) + + {valid, Enum.reverse(list)} + end + + defp validate(term), do: {false, term} +end diff --git a/lib/beacon/types/site.ex b/lib/beacon/types/site.ex index f3b315cb..3175de36 100644 --- a/lib/beacon/types/site.ex +++ b/lib/beacon/types/site.ex @@ -43,21 +43,17 @@ defmodule Beacon.Types.Site do @doc false def type, do: :atom - @doc false - def cast(:any, site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} - def cast(:any, site) when is_atom(site), do: {:ok, site} - def cast(:any, _), do: :error - @doc false def cast(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} def cast(site) when is_atom(site), do: {:ok, site} - def cast(_), do: :error - - @doc false - def load(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} + def cast(site), do: {:error, message: "invalid site #{inspect(site)}"} @doc false def dump(site) when is_binary(site), do: {:ok, site} def dump(site) when is_atom(site), do: {:ok, Atom.to_string(site)} - def dump(_), do: :error + def dump(_site), do: :error + + @doc false + def load(site) when is_binary(site), do: {:ok, String.to_existing_atom(site)} + def load(_site), do: :error end diff --git a/test/beacon/content_test.exs b/test/beacon/content_test.exs index f5fe581e..98295f11 100644 --- a/test/beacon/content_test.exs +++ b/test/beacon/content_test.exs @@ -262,6 +262,19 @@ defmodule Beacon.ContentTest do assert_receive :lifecycle_after_create_page assert_receive :lifecycle_after_publish_page end + + test "save raw_schema" do + layout = layout_fixture(site: :raw_schema_test) + + assert %Page{raw_schema: [%{"foo" => "bar"}]} = + Content.create_page!(%{ + site: "my_site", + path: "/", + template: "

page

", + layout_id: layout.id, + raw_schema: [%{"foo" => "bar"}] + }) + end end describe "snippets" do diff --git a/test/beacon/types/atom_test.exs b/test/beacon/types/atom_test.exs new file mode 100644 index 00000000..c05bb260 --- /dev/null +++ b/test/beacon/types/atom_test.exs @@ -0,0 +1,24 @@ +defmodule Beacon.Types.AtomTest do + use ExUnit.Case, async: true + + alias Beacon.Types.Atom + + _ = :site + + test "cast" do + assert Atom.cast("site") == {:ok, :site} + assert Atom.cast(:site) == {:ok, :site} + assert Atom.cast(0) == {:error, [message: "invalid site 0"]} + end + + test "dump" do + assert Atom.dump("site") == {:ok, "site"} + assert Atom.dump(:site) == {:ok, "site"} + assert Atom.dump(0) == :error + end + + test "load" do + assert Atom.load("site") == {:ok, :site} + assert Atom.load(0) == :error + end +end diff --git a/test/beacon/types/binary_test.exs b/test/beacon/types/binary_test.exs new file mode 100644 index 00000000..2fe8f759 --- /dev/null +++ b/test/beacon/types/binary_test.exs @@ -0,0 +1,23 @@ +defmodule Beacon.Types.BinaryTest do + use ExUnit.Case, async: true + + alias Beacon.Types.Binary + + @term %{"foo" => :bar} + @binary :erlang.term_to_binary(@term) + + test "cast" do + assert Binary.cast(@binary) == {:ok, @binary} + assert Binary.cast(@term) == {:ok, @binary} + end + + test "dump" do + assert Binary.dump(@binary) == {:ok, @binary} + assert Binary.dump(@term) == {:ok, @binary} + end + + test "load" do + assert Binary.load(@binary) == {:ok, @term} + assert Binary.load(@term) == :error + end +end diff --git a/test/beacon/types/json_array_map_test.exs b/test/beacon/types/json_array_map_test.exs new file mode 100644 index 00000000..059fb4e0 --- /dev/null +++ b/test/beacon/types/json_array_map_test.exs @@ -0,0 +1,28 @@ +defmodule Beacon.Types.JsonArrayMapTest do + use ExUnit.Case, async: true + + alias Beacon.Types.JsonArrayMap + + @map %{"foo" => :bar} + + test "cast" do + assert JsonArrayMap.cast(@map) == {:ok, [@map]} + assert JsonArrayMap.cast([@map]) == {:ok, [@map]} + assert JsonArrayMap.cast([]) == {:ok, []} + assert JsonArrayMap.cast(nil) == {:error, [{:message, "expected a list of map or a map, got: nil"}]} + assert JsonArrayMap.cast([1]) == {:error, [{:message, "expected a list of map or a map, got: [1]"}]} + end + + test "dump" do + assert JsonArrayMap.dump(@map) == {:ok, [@map]} + assert JsonArrayMap.dump([@map]) == {:ok, [@map]} + assert JsonArrayMap.dump([]) == {:ok, []} + assert JsonArrayMap.dump(nil) == :error + assert JsonArrayMap.dump([1]) == :error + end + + test "load" do + assert JsonArrayMap.load([@map]) == {:ok, [@map]} + assert JsonArrayMap.load(@map) == :error + end +end diff --git a/test/beacon/types/site_test.exs b/test/beacon/types/site_test.exs new file mode 100644 index 00000000..924e4fb0 --- /dev/null +++ b/test/beacon/types/site_test.exs @@ -0,0 +1,27 @@ +defmodule Beacon.Types.SiteTest do + use ExUnit.Case, async: true + + alias Beacon.Types.Site + import Beacon.Types.Site, only: [valid?: 1] + + doctest Site, only: [valid?: 1] + + _ = :site + + test "cast" do + assert Site.cast("site") == {:ok, :site} + assert Site.cast(:site) == {:ok, :site} + assert Site.cast(0) == {:error, [message: "invalid site 0"]} + end + + test "dump" do + assert Site.dump("site") == {:ok, "site"} + assert Site.dump(:site) == {:ok, "site"} + assert Site.dump(0) == :error + end + + test "load" do + assert Site.load("site") == {:ok, :site} + assert Site.load(0) == :error + end +end