ユユユユユ

webエンジニアです

Keyword で適当な引数を受け取るプラクティス

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