diff --git a/README.md b/README.md index 273043e..ea7a842 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,13 @@ Extra options: App config can be used to set a global default to apply to all resources unless overridden in their individual config, or the LiveAdmin instance. -Extra options: +When set at the application level, resource-level keys are read at **compile time**, and must be configured in `config/config.exs` (or another compile-time config file). Setting any of them in `config/runtime.exs` (or otherwise at runtime) will raise on boot. + +Compile-time keys: `components`, `query_with`, `render_with`, `delete_with`, `create_with`, `update_with`, `validate_with`, `label_with`, `title_with`, `hidden_fields`, `immutable_fields`, `actions`, `tasks` + +Runtime keys: +* `ecto_repo` - module used to execute queries (Required) * `session_store` - a module implementing the `LiveAdmin.Session.Store` behavior, used to persist session data (default: LiveAdmin.Session.Agent) * `css_overrides` - a binary or MFA identifying a function that returns CSS to be appended to app css * `gettext_backend` - a module implementing the [Gettext API](https://hexdocs.pm/gettext/Gettext.html#module-gettext-api) that will be used for translations diff --git a/README.md.eex b/README.md.eex index 273043e..ea7a842 100644 --- a/README.md.eex +++ b/README.md.eex @@ -117,8 +117,13 @@ Extra options: App config can be used to set a global default to apply to all resources unless overridden in their individual config, or the LiveAdmin instance. -Extra options: +When set at the application level, resource-level keys are read at **compile time**, and must be configured in `config/config.exs` (or another compile-time config file). Setting any of them in `config/runtime.exs` (or otherwise at runtime) will raise on boot. + +Compile-time keys: `components`, `query_with`, `render_with`, `delete_with`, `create_with`, `update_with`, `validate_with`, `label_with`, `title_with`, `hidden_fields`, `immutable_fields`, `actions`, `tasks` + +Runtime keys: +* `ecto_repo` - module used to execute queries (Required) * `session_store` - a module implementing the `LiveAdmin.Session.Store` behavior, used to persist session data (default: LiveAdmin.Session.Agent) * `css_overrides` - a binary or MFA identifying a function that returns CSS to be appended to app css * `gettext_backend` - a module implementing the [Gettext API](https://hexdocs.pm/gettext/Gettext.html#module-gettext-api) that will be used for translations diff --git a/lib/application.ex b/lib/application.ex index b1c55dc..a7faef4 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -1,6 +1,38 @@ defmodule LiveAdmin.Application do use Application + @compile_time_app_keys [ + :components, + :query_with, + :render_with, + :delete_with, + :create_with, + :update_with, + :validate_with, + :label_with, + :title_with, + :hidden_fields, + :immutable_fields, + :actions, + :tasks + ] + + @compile_time_app_config %{ + components: Application.compile_env(:live_admin, :components), + query_with: Application.compile_env(:live_admin, :query_with), + render_with: Application.compile_env(:live_admin, :render_with), + delete_with: Application.compile_env(:live_admin, :delete_with), + create_with: Application.compile_env(:live_admin, :create_with), + update_with: Application.compile_env(:live_admin, :update_with), + validate_with: Application.compile_env(:live_admin, :validate_with), + label_with: Application.compile_env(:live_admin, :label_with), + title_with: Application.compile_env(:live_admin, :title_with), + hidden_fields: Application.compile_env(:live_admin, :hidden_fields), + immutable_fields: Application.compile_env(:live_admin, :immutable_fields), + actions: Application.compile_env(:live_admin, :actions), + tasks: Application.compile_env(:live_admin, :tasks) + } + def start(_type, _args) do opts = [strategy: :one_for_one, name: LiveAdmin.Supervisor] @@ -14,9 +46,32 @@ defmodule LiveAdmin.Application do NimbleOptions.validate!(Application.get_all_env(:live_admin), global_options_schema) + validate_compile_time_config!() + Supervisor.start_link(children(), opts) end + @doc false + def validate_compile_time_config! do + @compile_time_app_keys + |> Enum.filter(fn key -> + Map.fetch!(@compile_time_app_config, key) != Application.get_env(:live_admin, key) + end) + |> case do + [] -> + :ok + + mismatches -> + raise """ + The following :live_admin config keys have been set at runtime, but they must be set at compile time: + + #{Enum.map_join(mismatches, "\n", &" * #{inspect(&1)}")} + + Move these into config/config.exs (or another compile-time config file). + """ + end + end + defp children do [ {LiveAdmin.Session.Agent, %{}}, diff --git a/lib/live_admin.ex b/lib/live_admin.ex index 900cec3..9215220 100644 --- a/lib/live_admin.ex +++ b/lib/live_admin.ex @@ -121,6 +121,8 @@ defmodule LiveAdmin do Used internally to validate configuration in apps using LiveAdmin. + When set at the application level, every option in this schema *except* `ecto_repo` is read at compile time. Configure them in `config/config.exs` (or another compile-time config file); setting them at runtime will raise on boot. + Supported options: #{@options_schema |> NimbleOptions.new!() |> NimbleOptions.docs()} """ diff --git a/lib/live_admin/components/nav.ex b/lib/live_admin/components/nav.ex index 93f96be..b4bfea9 100644 --- a/lib/live_admin/components/nav.ex +++ b/lib/live_admin/components/nav.ex @@ -16,7 +16,7 @@ defmodule LiveAdmin.Components.Nav do match?( %{ metadata: %{ - phoenix_live_view: {_, _, _, %{extra: %{session: {_, _, [^base_path, _]}}}} + phoenix_live_view: {_, _, _, %{extra: %{session: {_, _, [^base_path, _, _]}}}} } }, r diff --git a/lib/live_admin/router.ex b/lib/live_admin/router.ex index 03ef5b5..1ad834f 100644 --- a/lib/live_admin/router.ex +++ b/lib/live_admin/router.ex @@ -23,10 +23,27 @@ defmodule LiveAdmin.Router do @base_path Path.join(["/", current_path, unquote(path)]) @__live_admin_scope_opts__ unquote(opts) + @__live_admin_app_config__ [ + components: Application.compile_env(:live_admin, :components, []), + query_with: Application.compile_env(:live_admin, :query_with), + render_with: Application.compile_env(:live_admin, :render_with), + delete_with: Application.compile_env(:live_admin, :delete_with), + create_with: Application.compile_env(:live_admin, :create_with), + update_with: Application.compile_env(:live_admin, :update_with), + validate_with: Application.compile_env(:live_admin, :validate_with), + label_with: Application.compile_env(:live_admin, :label_with), + title_with: Application.compile_env(:live_admin, :title_with), + hidden_fields: Application.compile_env(:live_admin, :hidden_fields, []), + immutable_fields: Application.compile_env(:live_admin, :immutable_fields, []), + actions: Application.compile_env(:live_admin, :actions, []), + tasks: Application.compile_env(:live_admin, :tasks, []) + ] scope unquote(path), alias: false, as: false do live_session :"live_admin_#{@base_path}", - session: {unquote(__MODULE__), :build_session, [@base_path, unquote(opts)]}, + session: + {unquote(__MODULE__), :build_session, + [@base_path, unquote(opts), @__live_admin_app_config__]}, root_layout: {LiveAdmin.View, :layout}, layout: {LiveAdmin.View, :app}, on_mount: {unquote(__MODULE__), :assign_options} do @@ -58,9 +75,7 @@ defmodule LiveAdmin.Router do LiveAdmin.Router.__validate_config__!( resource_mod, @__live_admin_scope_opts__, - create_with: Application.compile_env(:live_admin, :create_with), - update_with: Application.compile_env(:live_admin, :update_with), - components: Application.compile_env(:live_admin, :components, []) + @__live_admin_app_config__ ) full_path = Path.join(@base_path, path) @@ -112,7 +127,7 @@ defmodule LiveAdmin.Router do end) end - def build_session(conn, base_path, opts) do + def build_session(conn, base_path, opts, app_config) do opts_schema = LiveAdmin.base_configs_schema() ++ [title: [type: :string, default: "LiveAdmin"], on_mount: [type: {:tuple, [:atom, :atom]}]] @@ -128,7 +143,7 @@ defmodule LiveAdmin.Router do index: LiveAdmin.Components.Container.Index, show: LiveAdmin.Components.Container.Show ], - Application.get_env(:live_admin, :components, []) + Keyword.get(app_config, :components, []) ) opts = @@ -139,21 +154,18 @@ defmodule LiveAdmin.Router do Keyword.merge(default_components, Keyword.get(opts, :components, [])) ) |> Keyword.put_new(:ecto_repo, Application.get_env(:live_admin, :ecto_repo)) - |> Keyword.put_new(:render_with, Application.get_env(:live_admin, :render_with)) - |> Keyword.put_new(:delete_with, Application.get_env(:live_admin, :delete_with)) - |> Keyword.put_new(:create_with, Application.get_env(:live_admin, :create_with)) - |> Keyword.put_new(:query_with, Application.get_env(:live_admin, :query_with)) - |> Keyword.put_new(:update_with, Application.get_env(:live_admin, :update_with)) - |> Keyword.put_new(:label_with, Application.get_env(:live_admin, :label_with)) - |> Keyword.put_new(:title_with, Application.get_env(:live_admin, :title_with)) - |> Keyword.put_new(:validate_with, Application.get_env(:live_admin, :validate_with)) - |> Keyword.put_new(:hidden_fields, Application.get_env(:live_admin, :hidden_fields, [])) - |> Keyword.put_new( - :immutable_fields, - Application.get_env(:live_admin, :immutable_fields, []) - ) - |> Keyword.put_new(:actions, Application.get_env(:live_admin, :actions, [])) - |> Keyword.put_new(:tasks, Application.get_env(:live_admin, :tasks, [])) + |> Keyword.put_new(:render_with, Keyword.get(app_config, :render_with)) + |> Keyword.put_new(:delete_with, Keyword.get(app_config, :delete_with)) + |> Keyword.put_new(:create_with, Keyword.get(app_config, :create_with)) + |> Keyword.put_new(:query_with, Keyword.get(app_config, :query_with)) + |> Keyword.put_new(:update_with, Keyword.get(app_config, :update_with)) + |> Keyword.put_new(:label_with, Keyword.get(app_config, :label_with)) + |> Keyword.put_new(:title_with, Keyword.get(app_config, :title_with)) + |> Keyword.put_new(:validate_with, Keyword.get(app_config, :validate_with)) + |> Keyword.put_new(:hidden_fields, Keyword.get(app_config, :hidden_fields, [])) + |> Keyword.put_new(:immutable_fields, Keyword.get(app_config, :immutable_fields, [])) + |> Keyword.put_new(:actions, Keyword.get(app_config, :actions, [])) + |> Keyword.put_new(:tasks, Keyword.get(app_config, :tasks, [])) %{ "session_id" => LiveAdmin.session_store().init!(conn), diff --git a/test/live_admin/application_test.exs b/test/live_admin/application_test.exs new file mode 100644 index 0000000..b8f80a1 --- /dev/null +++ b/test/live_admin/application_test.exs @@ -0,0 +1,24 @@ +defmodule LiveAdmin.ApplicationTest do + use ExUnit.Case, async: false + + describe "validate_compile_time_config! when a resource option is set at runtime" do + setup do + Application.put_env(:live_admin, :create_with, {RuntimeMod, :runtime_fun}) + on_exit(fn -> Application.delete_env(:live_admin, :create_with) end) + + :ok + end + + test "raises pointing the user to compile-time config" do + assert_raise RuntimeError, ~r/create_with/, fn -> + LiveAdmin.Application.validate_compile_time_config!() + end + end + end + + describe "validate_compile_time_config! when no resource options diverge between compile and runtime env" do + test "returns :ok" do + assert :ok == LiveAdmin.Application.validate_compile_time_config!() + end + end +end diff --git a/test/live_admin/router_test.exs b/test/live_admin/router_test.exs index ecb5045..4222403 100644 --- a/test/live_admin/router_test.exs +++ b/test/live_admin/router_test.exs @@ -1,8 +1,62 @@ defmodule LiveAdmin.RouterTest do use ExUnit.Case, async: true + import Mox + alias LiveAdmin.Router + setup :verify_on_exit! + + describe "build_session/4 with a compile-time resource option in app_config" do + setup do + stub_with(LiveAdminTest.MockSession, LiveAdminTest.StubSession) + + session = + Router.build_session( + %Plug.Conn{}, + "/admin", + [], + create_with: {SomeMod, :some_fun} + ) + + %{session: session} + end + + test "resolves the option from app_config", %{session: session} do + assert session["opts"][:create_with] == {SomeMod, :some_fun} + end + end + + describe "build_session/4 when a compile-time resource option is set in runtime Application env" do + setup do + stub_with(LiveAdminTest.MockSession, LiveAdminTest.StubSession) + + Application.put_env(:live_admin, :create_with, {RuntimeMod, :runtime_fun}) + on_exit(fn -> Application.delete_env(:live_admin, :create_with) end) + + session = Router.build_session(%Plug.Conn{}, "/admin", [], []) + + %{session: session} + end + + test "ignores the runtime value", %{session: session} do + refute session["opts"][:create_with] + end + end + + describe "build_session/4 list-typed resource options not provided in app_config" do + setup do + stub_with(LiveAdminTest.MockSession, LiveAdminTest.StubSession) + + session = Router.build_session(%Plug.Conn{}, "/admin", [], []) + %{session: session} + end + + test "default to an empty list", %{session: session} do + assert session["opts"][:hidden_fields] == [] + end + end + describe "create_with: false and custom :create component at the resource level" do test "raises ArgumentError" do assert_raise ArgumentError, ~r/create_with: false.*:create component/, fn ->