From bc97f6bc7516e4e578b15d0aa98e3feeda24ff3b Mon Sep 17 00:00:00 2001 From: KaFai Choi Date: Mon, 3 Feb 2025 18:28:04 +0700 Subject: [PATCH 1/3] This is a combination of 2 commits. update scrivener pointing to river forked one supporting simple page --- mix.exs | 3 ++- mix.lock | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index d2ea12e..9eb972c 100644 --- a/mix.exs +++ b/mix.exs @@ -38,7 +38,8 @@ defmodule Scrivener.Ecto.Mixfile do defp deps do [ - {:scrivener, "~> 2.4"}, + {:scrivener, + github: "RiverFinancial/scrivener", ref: "e21e9e83b4e101d2e3b5e24acb29289708746dac"}, {:ecto, "~> 3.12"}, {:ecto_sql, "~> 3.12", only: :test}, {:dialyxir, "~> 1.0", only: :dev}, diff --git a/mix.lock b/mix.lock index 53dd3d9..a333894 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, - "scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"}, + "scrivener": {:git, "https://github.com/RiverFinancial/scrivener.git", "e21e9e83b4e101d2e3b5e24acb29289708746dac", [ref: "e21e9e83b4e101d2e3b5e24acb29289708746dac"]}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } From 86ebcefa75d35243f55cb0c1d5559f92b1b66feb Mon Sep 17 00:00:00 2001 From: KaFai Choi Date: Mon, 3 Feb 2025 10:48:44 +0700 Subject: [PATCH 2/3] add page_type to Config. allow caller to get more efficient SimplePage --- lib/scrivener/paginater/ecto/query.ex | 56 ++- lib/scrivener/simple_page.ex | 53 ++ test/scrivener/paginator/ecto/query_test.exs | 496 ++++++++++++++++++- test/scrivener/simple_page_test.exs | 34 ++ 4 files changed, 628 insertions(+), 11 deletions(-) create mode 100644 lib/scrivener/simple_page.ex create mode 100644 test/scrivener/simple_page_test.exs diff --git a/lib/scrivener/paginater/ecto/query.ex b/lib/scrivener/paginater/ecto/query.ex index abbb2fb..65594c4 100644 --- a/lib/scrivener/paginater/ecto/query.ex +++ b/lib/scrivener/paginater/ecto/query.ex @@ -1,16 +1,18 @@ defimpl Scrivener.Paginater, for: Ecto.Query do import Ecto.Query - alias Scrivener.{Config, Page} + alias Scrivener.{Config, Page, SimplePage} @moduledoc false - @spec paginate(Ecto.Query.t(), Scrivener.Config.t()) :: Scrivener.Page.t() + @spec paginate(Ecto.Query.t(), Scrivener.Config.t()) :: + Scrivener.Page.t() | Scrivener.SimplePage.t() def paginate(query, %Config{ - page_size: page_size, - page_number: page_number, + page_type: :normal, module: repo, caller: caller, + page_number: page_number, + page_size: page_size, options: options }) do total_entries = @@ -26,25 +28,59 @@ defimpl Scrivener.Paginater, for: Ecto.Query do page_number = if allow_overflow_page_number, do: page_number, else: min(total_pages, page_number) + entries = + if page_number > total_pages, + do: [], + else: entries(query, repo, page_number, page_size, options) + %Page{ page_size: page_size, page_number: page_number, - entries: entries(query, repo, page_number, total_pages, page_size, options), + entries: entries, total_entries: total_entries, total_pages: total_pages } end - defp entries(_query, _repo, page_number, total_pages, _page_size, _options) - when page_number > total_pages, - do: [] + def paginate(query, %Config{ + page_type: :simple, + module: repo, + page_number: page_number, + page_size: page_size, + options: options + }) do + entries_with_maybe_one_extra = + entries(query, repo, page_number, page_size, options, extra_entry_size: 1) + + {entries, has_more} = + if length(entries_with_maybe_one_extra) > page_size do + entries = + entries_with_maybe_one_extra + |> Enum.reverse() + |> tl() + |> Enum.reverse() + + {entries, true} + else + {entries_with_maybe_one_extra, false} + end + + %SimplePage{ + page_size: page_size, + page_number: page_number, + entries: entries, + has_more: has_more + } + end - defp entries(query, repo, page_number, _total_pages, page_size, options) do + defp entries(query, repo, page_number, page_size, options, opts \\ []) do + extra_entry_size = Keyword.get(opts, :extra_entry_size, 0) offset = Keyword.get_lazy(options, :offset, fn -> page_size * (page_number - 1) end) + limit = page_size + extra_entry_size query |> offset(^offset) - |> limit(^page_size) + |> limit(^limit) |> repo.all(options) end diff --git a/lib/scrivener/simple_page.ex b/lib/scrivener/simple_page.ex new file mode 100644 index 0000000..9274378 --- /dev/null +++ b/lib/scrivener/simple_page.ex @@ -0,0 +1,53 @@ +defmodule Scrivener.SimplePage do + @moduledoc """ + Similar as `Scrivener.Page`, but without the `total_entries` and `total_pages` fields and with a `has_more` field instead. + """ + + defstruct [:page_number, :page_size, :has_more, entries: []] + + @type t :: %__MODULE__{ + entries: list(), + page_number: pos_integer(), + page_size: integer(), + has_more: boolean() + } + @type t(entry) :: %__MODULE__{ + entries: list(entry), + page_number: pos_integer(), + page_size: integer(), + has_more: boolean() + } + + defimpl Enumerable do + @spec count(Scrivener.SimplePage.t()) :: {:error, Enumerable.Scrivener.SimplePage} + def count(_page), do: {:error, __MODULE__} + + @spec member?(Scrivener.SimplePage.t(), term) :: {:error, Enumerable.Scrivener.SimplePage} + def member?(_page, _value), do: {:error, __MODULE__} + + @spec reduce(Scrivener.SimplePage.t(), Enumerable.acc(), Enumerable.reducer()) :: + Enumerable.result() + def reduce(%Scrivener.SimplePage{entries: entries}, acc, fun) do + Enumerable.reduce(entries, acc, fun) + end + + @spec slice(Scrivener.SimplePage.t()) :: {:error, Enumerable.Scrivener.SimplePage} + def slice(_page), do: {:error, __MODULE__} + end + + defimpl Collectable do + @spec into(Scrivener.SimplePage.t()) :: + {term, (term, Collectable.command() -> Scrivener.SimplePage.t() | term)} + def into(original) do + original_entries = original.entries + impl = Collectable.impl_for(original_entries) + {_, entries_fun} = impl.into(original_entries) + + fun = fn page, command -> + %{page | entries: entries_fun.(page.entries, command)} + end + + {original, fun} + end + end +end diff --git a/test/scrivener/paginator/ecto/query_test.exs b/test/scrivener/paginator/ecto/query_test.exs index 671eecf..52ff7f2 100644 --- a/test/scrivener/paginator/ecto/query_test.exs +++ b/test/scrivener/paginator/ecto/query_test.exs @@ -49,7 +49,7 @@ defmodule Scrivener.Paginator.Ecto.QueryTest do end) end - describe "paginate" do + describe "paginate with normal page type" do test "paginates an unconstrained query" do create_posts() @@ -461,6 +461,500 @@ defmodule Scrivener.Paginator.Ecto.QueryTest do end end + describe "paginate with simple page type" do + test "paginates the same as normal page type" do + create_posts() + + # There are total 8 posts, so we should have 4 pages + page_size = 2 + + for page_number <- 1..4 do + simple_page = + Post + |> Scrivener.Ecto.Repo.paginate( + page_type: :simple, + page_size: page_size, + page_number: page_number + ) + + normal_page = + Post + |> Scrivener.Ecto.Repo.paginate( + page_type: :normal, + page_size: page_size, + page_number: page_number + ) + + assert simple_page.page_size == normal_page.page_size + assert simple_page.page_number == normal_page.page_number + assert simple_page.entries == normal_page.entries + assert simple_page.has_more == normal_page.total_pages >= page_number + end + end + + test "paginates an unconstrained query" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = Post |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "page information is correct with no results" do + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: false + } = Post |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "uses defaults from the repo" do + posts = create_posts() + + expected_entries = Enum.take(posts, 5) + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + entries: ^expected_entries, + has_more: true + } = Post |> Post.published() |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "it handles preloads" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> Post.published() + |> preload(:comments) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "it handles offsets" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: false + } = + Post + |> Post.unpublished() + |> Scrivener.Ecto.Repo.paginate(options: [offset: 1], page_type: :simple) + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(options: [offset: 2], page_type: :simple) + end + + test "it handles complex selects" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> join(:left, [p], c in assoc(p, :comments)) + |> group_by([p], p.id) + |> select([p], sum(p.id)) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "it handles complex order_by" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> select([p], fragment("? as aliased_title", p.title)) + |> order_by([p], fragment("aliased_title")) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "can be provided the current page and page size as a params map" do + posts = create_posts() + expected_entries = Enum.drop(posts, 3) + + assert %Scrivener.SimplePage{ + page_size: 3, + page_number: 2, + entries: ^expected_entries, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(%{ + "page" => "2", + "page_size" => "3", + "page_type" => :simple + }) + end + + test "can be provided the current page and page size as options" do + posts = create_posts() + + expected_entries = Enum.drop(posts, 3) + + assert %Scrivener.SimplePage{ + page_size: 3, + page_number: 2, + entries: ^expected_entries, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(page: 2, page_size: 3, page_type: :simple) + end + + test "can be provided the caller as options" do + create_posts() + parent = self() + + task = + Task.async(fn -> + Post + |> Scrivener.Ecto.Repo.paginate(caller: parent, page_type: :simple) + end) + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = Task.await(task) + end + + test "can be provided the caller as a map" do + create_posts() + + parent = self() + + task = + Task.async(fn -> + Post + |> Scrivener.Ecto.Repo.paginate(%{"caller" => parent, "page_type" => :simple}) + end) + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = Task.await(task) + end + + test "will respect the max_page_size configuration" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 10 + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(%{ + "page" => "1", + "page_size" => "20", + "page_type" => :simple + }) + end + + test "will ignore total_entries passed to paginate" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(options: [total_entries: 130], page_type: :simple) + end + + test "will return an empty list if page_numer is too large" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 3, + has_more: false, + entries: [] + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate(page: 3, page_type: :simple) + end + + test "allows overflow page numbers if option is specified" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 3, + has_more: false, + entries: [] + } = + Post + |> Post.published() + |> Scrivener.Ecto.Repo.paginate( + page: 3, + options: [allow_overflow_page_number: true], + page_type: :simple + ) + end + + test "can be used on a table with any primary key" do + create_key_values() + + assert %Scrivener.SimplePage{ + page_size: 2, + page_number: 1, + has_more: true + } = + KeyValue + |> KeyValue.zero() + |> Scrivener.Ecto.Repo.paginate(page_size: 2, page_type: :simple) + end + + test "can be used with a group by clause" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> join(:left, [p], c in assoc(p, :comments)) + |> group_by([p], p.id) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "can be used with a group by clause on field other than id" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> group_by([p], p.body) + |> select([p], p.body) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "can be used with a group by clause on field on joined table" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + entries: entries, + page_number: 1, + has_more: false + } = + Post + |> join(:inner, [p], c in assoc(p, :comments)) + |> group_by([p, c], c.body) + |> select([p, c], {c.body, count("*")}) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + + assert length(entries) == 2 + end + + test "can be used with compound group by clause" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + entries: entries, + has_more: false + } = + Post + |> join(:inner, [p], c in assoc(p, :comments)) + |> group_by([p, c], [c.body, p.title]) + |> select([p, c], {c.body, p.title, count("*")}) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + + assert length(entries) == 2 + end + + test "can be used with combinations" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> Post.published() + |> union(^Post.unpublished(Post)) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "can be provided a Scrivener.Config directly" do + posts = create_posts() + + expected_entries = Enum.drop(posts, 4) + + config = %Scrivener.Config{ + module: Scrivener.Ecto.Repo, + page_number: 2, + page_size: 4, + page_type: :simple, + options: [] + } + + assert %Scrivener.SimplePage{ + page_size: 4, + page_number: 2, + entries: ^expected_entries, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.paginate(config) + end + + test "can be provided a keyword directly" do + posts = create_posts() + expected_entries = Enum.drop(posts, 4) + + assert %Scrivener.SimplePage{ + page_size: 4, + page_number: 2, + entries: ^expected_entries, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.paginate( + module: Scrivener.Ecto.Repo, + page: 2, + page_size: 4, + page_type: :simple + ) + end + + test "can be provided a map directly" do + posts = create_posts() + expected_entries = Enum.drop(posts, 4) + + assert %Scrivener.SimplePage{ + page_size: 4, + page_number: 2, + entries: ^expected_entries, + has_more: false + } = + Post + |> Post.published() + |> Scrivener.paginate(%{ + "module" => Scrivener.Ecto.Repo, + "page" => 2, + "page_size" => 4, + "page_type" => :simple + }) + end + + test "pagination plays nice with distinct on in the query" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> distinct([p], asc: p.title, asc: p.inserted_at) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "pagination plays nice with absolute distinct in the query" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> distinct(true) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "pagination plays nice with a singular distinct in the query" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Post + |> distinct(:id) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + end + + test "pagination plays nice with absolute distinct on a join query" do + create_posts() + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + entries: entries, + has_more: false + } = + Post + |> distinct(true) + |> join(:inner, [p], c in assoc(p, :comments)) + |> Scrivener.Ecto.Repo.paginate(page_type: :simple) + + assert length(entries) == 1 + end + + test "can specify prefix" do + create_users(6, "tenant_1") + create_users(2, "tenant_2") + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: true + } = + Scrivener.Ecto.Repo.paginate(User, + options: [prefix: "tenant_1"], + page_type: :simple + ) + + assert %Scrivener.SimplePage{ + page_size: 5, + page_number: 1, + has_more: false + } = + Scrivener.Ecto.Repo.paginate(User, + options: [prefix: "tenant_2"], + page_type: :simple + ) + end + end + test "accepts repo options" do log = capture_log(fn -> Scrivener.Ecto.Repo.paginate(Post, options: [log: true]) end) diff --git a/test/scrivener/simple_page_test.exs b/test/scrivener/simple_page_test.exs new file mode 100644 index 0000000..b58e7f2 --- /dev/null +++ b/test/scrivener/simple_page_test.exs @@ -0,0 +1,34 @@ +defmodule Scrivener.SimplePageTest do + use Scrivener.TestCase + + alias Scrivener.SimplePage + alias Scrivener.Post + + describe "enumerable" do + test "implements enumerable" do + post1 = %Post{title: "post 1"} + post2 = %Post{title: "post 2"} + page = %SimplePage{entries: [post1, post2]} + + titles = Enum.map(page, &Map.get(&1, :title)) + + assert titles == ["post 1", "post 2"] + end + + test "behaviour when empty" do + assert [] = Enum.map(%SimplePage{}, &Map.get(&1, :title)) + end + end + + describe "collectable" do + @spec build_post_page(list(Post.t())) :: SimplePage.t(Post.t()) + def build_post_page(list), do: Enum.into(list, %SimplePage{}) + + test "implements collectable" do + post1 = %Post{title: "post 1"} + post2 = %Post{title: "post 2"} + + assert %SimplePage{entries: [^post1, ^post2]} = build_post_page([post1, post2]) + end + end +end From 3fe3b5af39e65bbb04b7cc7351bfc2563fc523c0 Mon Sep 17 00:00:00 2001 From: KaFai Choi Date: Wed, 5 Feb 2025 10:11:09 +0700 Subject: [PATCH 3/3] remove duplicated simple_page module --- lib/scrivener/simple_page.ex | 53 ----------------------------- test/scrivener/simple_page_test.exs | 34 ------------------ 2 files changed, 87 deletions(-) delete mode 100644 lib/scrivener/simple_page.ex delete mode 100644 test/scrivener/simple_page_test.exs diff --git a/lib/scrivener/simple_page.ex b/lib/scrivener/simple_page.ex deleted file mode 100644 index 9274378..0000000 --- a/lib/scrivener/simple_page.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Scrivener.SimplePage do - @moduledoc """ - Similar as `Scrivener.Page`, but without the `total_entries` and `total_pages` fields and with a `has_more` field instead. - """ - - defstruct [:page_number, :page_size, :has_more, entries: []] - - @type t :: %__MODULE__{ - entries: list(), - page_number: pos_integer(), - page_size: integer(), - has_more: boolean() - } - @type t(entry) :: %__MODULE__{ - entries: list(entry), - page_number: pos_integer(), - page_size: integer(), - has_more: boolean() - } - - defimpl Enumerable do - @spec count(Scrivener.SimplePage.t()) :: {:error, Enumerable.Scrivener.SimplePage} - def count(_page), do: {:error, __MODULE__} - - @spec member?(Scrivener.SimplePage.t(), term) :: {:error, Enumerable.Scrivener.SimplePage} - def member?(_page, _value), do: {:error, __MODULE__} - - @spec reduce(Scrivener.SimplePage.t(), Enumerable.acc(), Enumerable.reducer()) :: - Enumerable.result() - def reduce(%Scrivener.SimplePage{entries: entries}, acc, fun) do - Enumerable.reduce(entries, acc, fun) - end - - @spec slice(Scrivener.SimplePage.t()) :: {:error, Enumerable.Scrivener.SimplePage} - def slice(_page), do: {:error, __MODULE__} - end - - defimpl Collectable do - @spec into(Scrivener.SimplePage.t()) :: - {term, (term, Collectable.command() -> Scrivener.SimplePage.t() | term)} - def into(original) do - original_entries = original.entries - impl = Collectable.impl_for(original_entries) - {_, entries_fun} = impl.into(original_entries) - - fun = fn page, command -> - %{page | entries: entries_fun.(page.entries, command)} - end - - {original, fun} - end - end -end diff --git a/test/scrivener/simple_page_test.exs b/test/scrivener/simple_page_test.exs deleted file mode 100644 index b58e7f2..0000000 --- a/test/scrivener/simple_page_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Scrivener.SimplePageTest do - use Scrivener.TestCase - - alias Scrivener.SimplePage - alias Scrivener.Post - - describe "enumerable" do - test "implements enumerable" do - post1 = %Post{title: "post 1"} - post2 = %Post{title: "post 2"} - page = %SimplePage{entries: [post1, post2]} - - titles = Enum.map(page, &Map.get(&1, :title)) - - assert titles == ["post 1", "post 2"] - end - - test "behaviour when empty" do - assert [] = Enum.map(%SimplePage{}, &Map.get(&1, :title)) - end - end - - describe "collectable" do - @spec build_post_page(list(Post.t())) :: SimplePage.t(Post.t()) - def build_post_page(list), do: Enum.into(list, %SimplePage{}) - - test "implements collectable" do - post1 = %Post{title: "post 1"} - post2 = %Post{title: "post 2"} - - assert %SimplePage{entries: [^post1, ^post2]} = build_post_page([post1, post2]) - end - end -end