長ったらしい日記を昨日書いて、それをさっそく修正する気になった。 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