From 4143ce5027c8ea50241e90cf0a8fd0faac2ca2d8 Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit Date: Sun, 16 Jul 2023 10:55:08 +0900 Subject: [PATCH 1/2] move ecto-specific schema options to adapter --- lib/flop/adapter.ex | 2 ++ lib/flop/adapter/ecto.ex | 54 ++++++++++++++++++++++++++++++++++++++ lib/flop/nimble_schemas.ex | 16 +++++++++-- lib/flop/schema.ex | 39 ++++++++++++++++++--------- test/support/pet.ex | 46 ++++++++++++++++++-------------- 5 files changed, 123 insertions(+), 34 deletions(-) diff --git a/lib/flop/adapter.ex b/lib/flop/adapter.ex index 3cda922..6787424 100644 --- a/lib/flop/adapter.ex +++ b/lib/flop/adapter.ex @@ -27,4 +27,6 @@ defmodule Flop.Adapter do @callback count(queryable, opts) :: non_neg_integer @callback list(queryable, opts) :: [any] + + @callback schema_options() :: NimbleOptions.t() end diff --git a/lib/flop/adapter/ecto.ex b/lib/flop/adapter/ecto.ex index 22641d0..6de407b 100644 --- a/lib/flop/adapter/ecto.ex +++ b/lib/flop/adapter/ecto.ex @@ -35,6 +35,60 @@ defmodule Flop.Adapter.Ecto do :ilike_or ] + @schema_options [ + join_fields: [ + type: :keyword_list, + keys: [ + *: [ + type: + {:or, + [ + keyword_list: [ + binding: [type: :atom, required: true], + field: [type: :atom, required: true], + ecto_type: [type: :any], + path: [type: {:list, :atom}] + ], + tuple: [:atom, :atom] + ]} + ] + ] + ], + compound_fields: [ + type: :keyword_list, + keys: [ + *: [ + type: {:list, :atom} + ] + ] + ], + custom_fields: [ + type: :keyword_list, + keys: [ + *: [ + type: :keyword_list, + keys: [ + filter: [ + type: {:tuple, [:atom, :atom, :keyword_list]}, + required: true + ], + ecto_type: [type: :any], + bindings: [type: {:list, :atom}], + operators: [type: {:list, :atom}] + ] + ] + ] + ], + alias_fields: [ + type: {:list, :atom} + ] + ] + + @schema_options NimbleOptions.new!(@schema_options) + + @impl Flop.Adapter + def schema_options, do: @schema_options + @impl Flop.Adapter def apply_filter( query, diff --git a/lib/flop/nimble_schemas.ex b/lib/flop/nimble_schemas.ex index c567b19..e5c196c 100644 --- a/lib/flop/nimble_schemas.ex +++ b/lib/flop/nimble_schemas.ex @@ -30,6 +30,11 @@ defmodule Flop.NimbleSchemas do ] @schema_option [ + adapter: [type: :atom, default: Flop.Adapter.Ecto], + adapter_opts: [ + type: :keyword_list, + default: [] + ], filterable: [type: {:list, :atom}, required: true], sortable: [type: {:list, :atom}, required: true], default_order: [ @@ -107,11 +112,18 @@ defmodule Flop.NimbleSchemas do ] ] + @schema_option_schema @schema_option + def schema_option_schema, do: @schema_option_schema + @backend_option NimbleOptions.new!(@backend_option) @schema_option NimbleOptions.new!(@schema_option) - def validate!(opts, schema_id, module, caller) do - case NimbleOptions.validate(opts, schema(schema_id)) do + def validate!(opts, schema_id, module, caller) when is_atom(schema_id) do + validate!(opts, schema(schema_id), module, caller) + end + + def validate!(opts, %NimbleOptions{} = schema, module, caller) do + case NimbleOptions.validate(opts, schema) do {:ok, opts} -> opts diff --git a/lib/flop/schema.ex b/lib/flop/schema.ex index e5cc741..f603822 100644 --- a/lib/flop/schema.ex +++ b/lib/flop/schema.ex @@ -802,7 +802,22 @@ defimpl Flop.Schema, for: Any do __CALLER__.module ) - validate_options!(options, struct) + legacy_adapter_opts = + Keyword.take(options, [ + :alias_fields, + :compound_fields, + :custom_fields, + :join_fields + ]) + + adapter_schema = Keyword.fetch!(options, :adapter).schema_options() + + adapter_opts = + legacy_adapter_opts + |> Keyword.merge(Keyword.fetch!(options, :adapter_opts)) + |> NimbleSchemas.validate!(adapter_schema, Flop.Schema, __CALLER__.module) + + validate_options!(options, adapter_opts, struct) filterable_fields = Keyword.get(options, :filterable) sortable_fields = Keyword.get(options, :sortable) @@ -811,16 +826,16 @@ defimpl Flop.Schema, for: Any do pagination_types = Keyword.get(options, :pagination_types) default_pagination_type = Keyword.get(options, :default_pagination_type) default_order = Keyword.get(options, :default_order) - compound_fields = Keyword.get(options, :compound_fields, []) - alias_fields = Keyword.get(options, :alias_fields, []) + compound_fields = Keyword.get(adapter_opts, :compound_fields, []) + alias_fields = Keyword.get(adapter_opts, :alias_fields, []) custom_fields = - options + adapter_opts |> Keyword.get(:custom_fields, []) |> Enum.map(&normalize_custom_opts/1) join_fields = - options + adapter_opts |> Keyword.get(:join_fields, []) |> Enum.map(&normalize_join_opts/1) @@ -894,12 +909,12 @@ defimpl Flop.Schema, for: Any do end end - defp validate_options!(opts, struct) do - compound_fields = get_compound_fields(opts) - join_fields = get_join_fields(opts) + defp validate_options!(opts, adapter_opts, struct) do + compound_fields = get_compound_fields(adapter_opts) + join_fields = get_join_fields(adapter_opts) schema_fields = get_schema_fields(struct) - alias_fields = Keyword.get(opts, :alias_fields, []) - custom_fields = get_custom_fields(opts) + alias_fields = Keyword.get(adapter_opts, :alias_fields, []) + custom_fields = get_custom_fields(adapter_opts) all_fields = compound_fields ++ @@ -917,9 +932,9 @@ defimpl Flop.Schema, for: Any do validate_no_unknown_field!(opts[:filterable], all_fields, "filterable") validate_no_unknown_field!(opts[:sortable], all_fields, "sortable") validate_default_order!(opts[:default_order], opts[:sortable]) - validate_compound_fields!(opts[:compound_fields], all_fields) + validate_compound_fields!(adapter_opts[:compound_fields], all_fields) validate_alias_fields!(alias_fields, opts[:filterable]) - validate_custom_fields!(opts[:custom_fields], opts[:sortable]) + validate_custom_fields!(adapter_opts[:custom_fields], opts[:sortable]) end defp get_compound_fields(opts) do diff --git a/test/support/pet.ex b/test/support/pet.ex index eb2e31c..731635e 100644 --- a/test/support/pet.ex +++ b/test/support/pet.ex @@ -25,28 +25,34 @@ defmodule MyApp.Pet do ], sortable: [:name, :age, :owner_name, :owner_age], max_limit: 1000, - compound_fields: [ - full_name: [:family_name, :given_name], - pet_and_owner_name: [:name, :owner_name] - ], - join_fields: [ - owner_age: {:owner, :age}, - owner_name: [ - binding: :owner, - field: :name, - path: [:owner, :name], - ecto_type: :string + adapter_opts: [ + compound_fields: [ + full_name: [:family_name, :given_name], + pet_and_owner_name: [:name, :owner_name] ], - owner_tags: [binding: :owner, field: :tags, ecto_type: {:array, :string}] - ], - custom_fields: [ - custom: [ - filter: {__MODULE__, :test_custom_filter, [some: :options]}, - operators: [:==] + join_fields: [ + owner_age: {:owner, :age}, + owner_name: [ + binding: :owner, + field: :name, + path: [:owner, :name], + ecto_type: :string + ], + owner_tags: [ + binding: :owner, + field: :tags, + ecto_type: {:array, :string} + ] ], - reverse_name: [ - filter: {__MODULE__, :reverse_name_filter, []}, - ecto_type: :string + custom_fields: [ + custom: [ + filter: {__MODULE__, :test_custom_filter, [some: :options]}, + operators: [:==] + ], + reverse_name: [ + filter: {__MODULE__, :reverse_name_filter, []}, + ecto_type: :string + ] ] ] } From f3c6655a14baea3ba9e67f83b3938252165ecffe Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit Date: Sun, 16 Jul 2023 11:30:43 +0900 Subject: [PATCH 2/2] move ecto-specific backend options to adapter --- lib/flop.ex | 13 +++++++++++-- lib/flop/adapter.ex | 1 + lib/flop/adapter/ecto.ex | 12 ++++++++++++ lib/flop/nimble_schemas.ex | 7 ++++++- test/flop_test.exs | 29 +++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/flop.ex b/lib/flop.ex index e260852..934c05a 100644 --- a/lib/flop.ex +++ b/lib/flop.ex @@ -301,7 +301,15 @@ defmodule Flop do __CALLER__.module ) - opts = Keyword.put(opts, :adapter, Flop.Adapter.Ecto) + {legacy_adapter_opts, opts} = Keyword.split(opts, [:query_opts, :repo]) + adapter_schema = Keyword.fetch!(opts, :adapter).backend_options() + + adapter_opts = + legacy_adapter_opts + |> Keyword.merge(Keyword.fetch!(opts, :adapter_opts)) + |> NimbleSchemas.validate!(adapter_schema, Flop.Schema, __CALLER__.module) + + opts = Keyword.put(opts, :adapter_opts, adapter_opts) quote do @doc false @@ -455,7 +463,8 @@ defmodule Flop do | {:extra_opts, Keyword.t()} | private_option() - @typep private_option :: {:adapter, module} | {:backend, module} + @typep private_option :: + {:adapter, module} | {:adapter_opts, keyword} | {:backend, module} @type default_order :: %{ diff --git a/lib/flop/adapter.ex b/lib/flop/adapter.ex index 6787424..4643ddc 100644 --- a/lib/flop/adapter.ex +++ b/lib/flop/adapter.ex @@ -28,5 +28,6 @@ defmodule Flop.Adapter do @callback list(queryable, opts) :: [any] + @callback backend_options() :: NimbleOptions.t() @callback schema_options() :: NimbleOptions.t() end diff --git a/lib/flop/adapter/ecto.ex b/lib/flop/adapter/ecto.ex index 6de407b..10a1b9d 100644 --- a/lib/flop/adapter/ecto.ex +++ b/lib/flop/adapter/ecto.ex @@ -35,6 +35,11 @@ defmodule Flop.Adapter.Ecto do :ilike_or ] + @backend_options [ + repo: [required: true], + query_opts: [type: :keyword_list, default: []] + ] + @schema_options [ join_fields: [ type: :keyword_list, @@ -84,8 +89,12 @@ defmodule Flop.Adapter.Ecto do ] ] + @backend_options NimbleOptions.new!(@backend_options) @schema_options NimbleOptions.new!(@schema_options) + @impl Flop.Adapter + def backend_options, do: @backend_options + @impl Flop.Adapter def schema_options, do: @schema_options @@ -239,6 +248,9 @@ defmodule Flop.Adapter.Ecto do end defp apply_on_repo(repo_fn, flop_fn, args, opts) do + # use nested adapter_opts if set + opts = Flop.get_option(:adapter_opts, opts) || opts + repo = Flop.get_option(:repo, opts) || raise no_repo_error(flop_fn) opts = query_opts(opts) diff --git a/lib/flop/nimble_schemas.ex b/lib/flop/nimble_schemas.ex index e5c196c..b91c0de 100644 --- a/lib/flop/nimble_schemas.ex +++ b/lib/flop/nimble_schemas.ex @@ -2,6 +2,11 @@ defmodule Flop.NimbleSchemas do @moduledoc false @backend_option [ + adapter: [type: :atom, default: Flop.Adapter.Ecto], + adapter_opts: [ + type: :keyword_list, + default: [] + ], cursor_value_func: [type: {:fun, 2}], default_limit: [type: :integer, default: 50], max_limit: [type: :integer, default: 1000], @@ -26,7 +31,7 @@ defmodule Flop.NimbleSchemas do default: [:offset, :page, :first, :last] ], repo: [], - query_opts: [type: :keyword_list] + query_opts: [type: :keyword_list, default: []] ] @schema_option [ diff --git a/test/flop_test.exs b/test/flop_test.exs index 3f66530..f0b7a2d 100644 --- a/test/flop_test.exs +++ b/test/flop_test.exs @@ -30,6 +30,12 @@ defmodule FlopTest do use Flop, repo: Flop.Repo, default_limit: 35 end + defmodule TestProviderNested do + use Flop, + adapter_opts: [repo: Flop.Repo], + default_limit: 35 + end + describe "ordering" do test "adds order_by to query if set" do pets = insert_list(20, :pet) @@ -1961,6 +1967,29 @@ defmodule FlopTest do end end + describe "__using__/1 with nested adapter options" do + test "defines wrapper functions that pass default options" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 35}}} = + TestProviderNested.validate_and_run(Pet, %{}) + end + + test "allows to override defaults" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 30}}} = + TestProviderNested.validate_and_run(Pet, %{page_size: 30}) + end + + test "passes backend module" do + assert {:ok, {_, %Meta{backend: TestProviderNested, opts: opts}}} = + TestProviderNested.validate_and_run(Pet, %{}) + + assert Keyword.get(opts, :backend) == TestProviderNested + end + end + describe "get_option/3" do test "returns value from option list" do # sanity check