From 0514840d014e1d81ad10d1468af716bbff701399 Mon Sep 17 00:00:00 2001 From: c4710n Date: Sat, 30 Dec 2023 21:14:41 +0800 Subject: [PATCH] feat: validate options with NimbleOptions --- lib/plug_locale/header.ex | 12 +-- lib/plug_locale/header/config.ex | 82 ++++++++---------- lib/plug_locale/web_browser.ex | 14 ++-- lib/plug_locale/web_browser/config.ex | 116 ++++++++++++-------------- mix.exs | 1 + mix.lock | 1 + test/plug_locale/web_browser_test.exs | 2 +- 7 files changed, 108 insertions(+), 120 deletions(-) diff --git a/lib/plug_locale/header.ex b/lib/plug_locale/header.ex index 0a56658..ac1c991 100644 --- a/lib/plug_locale/header.ex +++ b/lib/plug_locale/header.ex @@ -61,9 +61,11 @@ defmodule PlugLocale.Header do ## Options * `:default_locale` - the default locale. - * `:locales` - all the supported locales. Default to `[]`. + * `:locales` - all the supported locales. + Default to `[]`. * `:cast_locale_by` - specify the function for casting extracted or - detected locales. Default to `nil`. + detected locales. + Default to `nil`. * `:header_name` - the header for getting locale. Default to `"x-client-locale"`. * `:assign_key` - the key for putting value into `assigns` storage. @@ -103,7 +105,7 @@ defmodule PlugLocale.Header do plug `#{inspect(__MODULE__)}`, default_locale: "en", locales: ["en", "zh"], - sanitize_locale_by: &DemoWeb.I18n.cast_locale/1, + cast_locale_by: &DemoWeb.I18n.cast_locale/1, # ... """ @@ -131,12 +133,12 @@ defmodule PlugLocale.Header do end end - defp cast_locale(config, locale, opts \\ []) do + defp cast_locale(config, locale, opts) do default = Keyword.get(opts, :default, nil) if locale do casted_locale = - if is_function(config.cast_locale_by, 1), + if config.cast_locale_by, do: config.cast_locale_by.(locale), else: locale diff --git a/lib/plug_locale/header/config.ex b/lib/plug_locale/header/config.ex index 8223bca..749d0ea 100644 --- a/lib/plug_locale/header/config.ex +++ b/lib/plug_locale/header/config.ex @@ -1,64 +1,56 @@ defmodule PlugLocale.Header.Config do @moduledoc false - defstruct [ - :default_locale, - :locales, - :cast_locale_by, - :header_name, - :assign_key - ] - - @doc false - def new!(opts) when is_list(opts) do - default_locale = Keyword.get(opts, :default_locale) - locales = Keyword.get(opts, :locales, []) - cast_locale_by = Keyword.get(opts, :cast_locale_by, nil) - header_name = Keyword.get(opts, :header_name, "x-client-locale") - assign_key = Keyword.get(opts, :assign_key, :locale) - - [ - default_locale: default_locale, - locales: locales, - cast_locale_by: cast_locale_by, - header_name: header_name, - assign_key: assign_key + @opts_schema [ + default_locale: [ + type: :string, + required: true + ], + locales: [ + type: {:list, :string}, + default: [] + ], + cast_locale_by: [ + type: {:or, [nil, {:fun, 1}]}, + default: nil + ], + header_name: [ + type: :string, + default: "x-client-locale" + ], + assign_key: [ + type: :atom, + default: :locale ] - |> as_map!() - |> as_struct!() - end - - defp validate!(opts) do - keys = Keyword.keys(opts) - unset_keys = Enum.filter(keys, fn key -> Keyword.get(opts, key) == nil end) + ] - if unset_keys != [] do - raise RuntimeError, "following keys #{inspect(unset_keys)} should be set" - end + defstruct Keyword.keys(@opts_schema) + @doc false + def new!(opts) do opts + |> NimbleOptions.validate!(@opts_schema) + |> finalize() + |> as_struct!() end - defp as_map!(opts) do + defp finalize(opts) do default_locale = Keyword.fetch!(opts, :default_locale) locales = Keyword.fetch!(opts, :locales) - cast_locale_by = Keyword.fetch!(opts, :cast_locale_by) - header_name = Keyword.fetch!(opts, :header_name) - assign_key = Keyword.fetch!(opts, :assign_key) + locales = Enum.uniq([default_locale | locales]) - %{ - default_locale: default_locale, - locales: Enum.uniq([default_locale | locales]), - cast_locale_by: cast_locale_by, - header_name: header_name, - assign_key: assign_key - } + Keyword.merge(opts, locales: locales) end - defp as_struct!(config) do + defp as_struct!(opts) do default_struct = __MODULE__.__struct__() valid_keys = Map.keys(default_struct) - config = Map.take(config, valid_keys) + + config = + opts + |> Enum.into(%{}) + |> Map.take(valid_keys) + Map.merge(default_struct, config) end end diff --git a/lib/plug_locale/web_browser.ex b/lib/plug_locale/web_browser.ex index 027154f..00f94e2 100644 --- a/lib/plug_locale/web_browser.ex +++ b/lib/plug_locale/web_browser.ex @@ -85,21 +85,23 @@ defmodule PlugLocale.WebBrowser do ## Options * `:default_locale` - the default locale. - * `:locales` - all the supported locales. Default to `[]`. + * `:locales` - all the supported locales. + Default to `[]`. * `:detect_locale_from` - specify *the sources* and *the order of sources* for detecting locale. Available sources are `:query`, `:cookie`, `:referrer`, `:accept_language`. Default to `[:cookie, :referrer, :accept_language]`. * `:cast_locale_by` - specify the function for casting extracted or - detected locales. Default to `nil`. + detected locales. + Default to `nil`. * `:route_identifier` - the part for identifying locale in route. Default to `:locale`. * `:assign_key` - the key for putting value into `assigns` storage. - Default to the value of `:route_identifier` option. + Default to `:locale`. * `:query_key` - the key for getting locale from querystring. - Default to the **stringified** value of `:route_identifier` option. + Default to `"locale"`. * `:cookie_key` - the key for getting locale from cookie. - Default to `"preferred_locale"`. + Default to `"locale"`. ### about `:cast_locale_by` option @@ -463,7 +465,7 @@ defmodule PlugLocale.WebBrowser do if locale do casted_locale = - if is_function(config.cast_locale_by, 1), + if config.cast_locale_by, do: config.cast_locale_by.(locale), else: locale diff --git a/lib/plug_locale/web_browser/config.ex b/lib/plug_locale/web_browser/config.ex index dba9a6d..750f6c5 100644 --- a/lib/plug_locale/web_browser/config.ex +++ b/lib/plug_locale/web_browser/config.ex @@ -1,84 +1,74 @@ defmodule PlugLocale.WebBrowser.Config do @moduledoc false - defstruct [ - :default_locale, - :locales, - :detect_locale_from, - :cast_locale_by, - :route_identifier, - :path_param_key, - :assign_key, - :query_key, - :cookie_key - ] - - @doc false - def new!(opts) when is_list(opts) do - default_locale = Keyword.get(opts, :default_locale) - locales = Keyword.get(opts, :locales, []) - - detect_locale_from = - Keyword.get(opts, :detect_locale_from, [:cookie, :referrer, :accept_language]) - - cast_locale_by = Keyword.get(opts, :cast_locale_by, nil) - route_identifier = Keyword.get(opts, :route_identifier, :locale) - assign_key = Keyword.get(opts, :assign_key, route_identifier) - query_key = Keyword.get(opts, :query_key, to_string(route_identifier)) - cookie_key = Keyword.get(opts, :cookie_key, "preferred_locale") - - [ - default_locale: default_locale, - locales: locales, - detect_locale_from: detect_locale_from, - cast_locale_by: cast_locale_by, - route_identifier: route_identifier, - assign_key: assign_key, - query_key: query_key, - cookie_key: cookie_key + @opts_schema [ + default_locale: [ + type: :string, + required: true + ], + locales: [ + type: {:list, :string}, + default: [] + ], + detect_locale_from: [ + type: {:list, :atom}, + default: [:cookie, :referrer, :accept_language] + ], + cast_locale_by: [ + type: {:or, [nil, {:fun, 1}]}, + default: nil + ], + route_identifier: [ + type: :atom, + default: :locale + ], + assign_key: [ + type: :atom, + default: :locale + ], + query_key: [ + type: :string, + default: "locale" + ], + cookie_key: [ + type: :string, + default: "locale" ] - |> as_map!() - |> as_struct!() - end + ] - defp validate!(opts) do - keys = Keyword.keys(opts) - unset_keys = Enum.filter(keys, fn key -> Keyword.get(opts, key) == nil end) + @direct_fields Keyword.keys(@opts_schema) + @derived_fields [:path_param_key] - if unset_keys != [] do - raise RuntimeError, "following keys #{inspect(unset_keys)} should be set" - end + defstruct @direct_fields ++ @derived_fields + @doc false + def new!(opts) do opts + |> NimbleOptions.validate!(@opts_schema) + |> finalize() + |> as_struct!() end - defp as_map!(opts) do + defp finalize(opts) do default_locale = Keyword.fetch!(opts, :default_locale) locales = Keyword.fetch!(opts, :locales) - detect_locale_from = Keyword.fetch!(opts, :detect_locale_from) - cast_locale_by = Keyword.fetch!(opts, :cast_locale_by) + locales = Enum.uniq([default_locale | locales]) + route_identifier = Keyword.fetch!(opts, :route_identifier) - assign_key = Keyword.fetch!(opts, :assign_key) - query_key = Keyword.fetch!(opts, :query_key) - cookie_key = Keyword.fetch!(opts, :cookie_key) + path_param_key = to_string(route_identifier) - %{ - default_locale: default_locale, - locales: Enum.uniq([default_locale | locales]), - detect_locale_from: detect_locale_from, - cast_locale_by: cast_locale_by, - route_identifier: route_identifier, - path_param_key: to_string(route_identifier), - assign_key: assign_key, - query_key: query_key, - cookie_key: cookie_key - } + Keyword.merge(opts, locales: locales, path_param_key: path_param_key) end - defp as_struct!(config) do + defp as_struct!(opts) do default_struct = __MODULE__.__struct__() valid_keys = Map.keys(default_struct) - config = Map.take(config, valid_keys) + + config = + opts + |> Enum.into(%{}) + |> Map.take(valid_keys) + Map.merge(default_struct, config) end end diff --git a/mix.exs b/mix.exs index c829c5d..8426ae9 100644 --- a/mix.exs +++ b/mix.exs @@ -32,6 +32,7 @@ defmodule PlugLocale.MixProject do defp deps do [ {:plug, "~> 1.14"}, + {:nimble_options, "~> 1.0"}, {:ex_check, "~> 0.15.0", only: [:dev], runtime: false}, {:credo, ">= 0.0.0", only: [:dev], runtime: false}, {:dialyxir, ">= 0.0.0", only: [:dev], runtime: false}, diff --git a/mix.lock b/mix.lock index 5ee6137..33867d3 100644 --- a/mix.lock +++ b/mix.lock @@ -13,6 +13,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mix_audit": {:hex, :mix_audit, "2.1.1", "653aa6d8f291fc4b017aa82bdb79a4017903902ebba57960ef199cbbc8c008a1", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "541990c3ab3a7bb8c4aaa2ce2732a4ae160ad6237e5dcd5ad1564f4f85354db1"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, diff --git a/test/plug_locale/web_browser_test.exs b/test/plug_locale/web_browser_test.exs index 322585f..4a596d1 100644 --- a/test/plug_locale/web_browser_test.exs +++ b/test/plug_locale/web_browser_test.exs @@ -132,7 +132,7 @@ defmodule PlugLocale.WebBrowserTest do test "is redirected to a path detected from cookie" do conn = conn(:get, "/unknown-locale/posts/7") - |> put_resp_cookie("preferred_locale", "zh-Hans") + |> put_resp_cookie("locale", "zh-Hans") |> fetch_cookies() conn = DemoRouter.call(conn, @opts)