長ったらしい日記を昨日書いて、それをさっそく修正する気になった。 https://jnsato.hateblo.jp/entry/2022/10/30/010000
オプショナルなパラメータを Keyword で受け取るときに、パラメータごとにパターンマッチして正規化して…という手続きを大量の場合分けで書いていたのだった。
しかしデフォルト引数を opts \\ []
と宣言して、 Keyword.get/3
でそれを引っこ抜くようにすれば、ずいぶんコードが減る。これは https://github.com/BanchanArt/banchan というレポジトリの実装に教えられた。
つまりこう。
defmodule Hello.Catalog do
import Ecto.Query, warn: false
alias Hello.Catalog.Product
alias Hello.Repo
@default_page 1
@default_products_per_page 10
def list_products(opt \\ []) do
Product
|> Repo.paginate(
page: Keyword.get(opts, :page, @default_page),
page_size: Keyword.get(
opts,
:products_per_page,
@default_products_per_page
)
)
end
end
これは結局 Repo にパラメータを渡してバリデーションを委ねている。その点が気に入らないとぼやくエントリを書いてしまったけど、やっぱり手のひらを返す。それで実装を大幅にオミットすることができるのなら万々歳。実際 diff を出すとこうなる。
defmodule Hello.Repo do
otp_app: :hello,
adapter: Ecto.Adapters.Postgres
- use Scrivener, page_size: 10
+ use Scrivener, page_size: 10, max_page_size: 100
end
defmodule Hello.Catalog do
import Ecto.Query, warn: false
alias Hello.Catalog.Product
alias Hello.Repo
@default_page 1
@default_products_per_page 10
- @max_products_per_page 100
@default_sort [desc: :id]
@sort_expressions_supported [desc: :id]
@doc """
Returns the map of paginated products.
+ Optional parameters :page and :products_per_page can be provided.
"""
- def list_products,
- do: list_products(page: @default_page, products_per_page: @default_products_per_page)
-
- # Accommodate indivisual parameter
- def list_products(page: page),
- do: list_products(page: page, products_per_page: nil)
-
- def list_products(products_per_page: products_per_page),
- do: list_products(page: nil, products_per_page: products_per_page)
-
- # Accommodate nils.
- def list_products(page: nil, products_per_page: nil),
- do: list_products()
-
- def list_products(page: nil, products_per_page: products_per_page) do
- list_products(page: @default_page, products_per_page: products_per_page)
- end
-
- def list_products(page: page, products_per_page: nil) do
- list_products(page: page, products_per_page: @default_products_per_page)
- end
-
- # Validate arbitrary page input. Mostly user input string but others too.
- def list_products(page: page, products_per_page: products_per_page) when not is_integer(page) do
- case Integer.parse(page) do
- {int, ""} when int > 0 -> list_products(page: int, products_per_page: products_per_page)
- _ -> list_products(page: @default_page, products_per_page: products_per_page)
- end
- end
-
- # Validate arbitrary products_per_page input. Must define the max as well.
- def list_products(page: page, products_per_page: products_per_page)
- when not is_integer(products_per_page) do
- case Integer.parse(products_per_page) do
- {int, ""} when int > 0 and int <= @max_products_per_page ->
- list_products(page: page, products_per_page: int)
-
- _ ->
- list_products(page: page, products_per_page: @default_products_per_page)
- end
- end
- # Finally run query with normalized inputs.
- def list_products(page: page, products_per_page: products_per_page) do
+ def list_products(opts \\ []) do
# %Scrivener.Page{}
page =
Product
|> order_by(^@default_sort)
- |> Repo.paginate(page: page, page_size: products_per_page)
+ |> Repo.paginate(
+ page: Keyword.get(opts, :page, @default_page),
+ # Note that max_page_size is declared for repo.
+ page_size: Keyword.get(opts, :products_per_page, @default_products_per_page)
+ )
# Separate collection and the rest. Latter contains paging metadata.
{products, meta} = Map.pop(page, :entries)
# This is no longer compliant with %Scrivener.Page, so forget it.
meta =
Map.from_struct(meta)
# Then freely add some more metadata.
|> Map.put(:sort_expressions_applied, @default_sort)
|> Map.put(:sort_expressions_supported, @sort_expressions_supported)
# Is it better to declare custom struct to pattern match against?
# Not knowing a concise name let's just return bare map for now.
%{products: products, metadata: meta}
end
end