I have an Elixir application using Ecto, and I need to dynamically cast and validate embedded schemas based on a type field. My Discount schema has a field discount_params that can be different structures depending on the value of the type field.
I don’t want to change the field type from :map and want to leave it as it is.
Here is the base function:
defmodule Discounts.Discount do
schema "discounts" do
field :type, :string
field :global, :boolean
field :discount, :integer
field :discount_params, :map
field :latest_event, :string, virtual: true, default: ""
has_many :event_discounts, EventDiscount, on_replace: :delete
has_many :events, through: [:event_discounts, :event]
timestamps()
end
@type id_t() :: String.t() | non_neg_integer()
@type t ::
%__MODULE__{
id: non_neg_integer(),
type: String.t(),
global: boolean(),
discount: non_neg_integer(),
discount_params: %{String.t() => String.t() | non_neg_integer()},
inserted_at: Date.t(),
event_discounts: [EventDiscount.t()],
events: [Event.t()]
}
| %__MODULE__{}
@required_params ~w(type discount discount_params)a
@optional_params ~w(global)a
defp changeset(discount, params) do
discount
|> cast(params, @required_params ++ @optional_params)
|> cast_assoc(:event_discounts)
|> validate_required(@required_params)
|> validate_inclusion(:type, ["Package", "Volume"])
|> cast_discount_params(params)
end
defp cast_discount_params(changeset, params) do
type = get_field(changeset, :type)
schema_module = Module.concat(Discounts.DiscountType, DiscountParams.schema(type))
# return example Discounts.DiscountType.Volume.Params
if schema_module do
embed_params = Map.get(params, "discount_params", %{})
embed_changeset = schema_module.changeset(%schema_module{}, embed_params)
if embed_changeset.valid? do
params = apply_changes(embed_changeset)
change(changeset, discount_params: params)
else
Enum.reduce(embed_changeset.errors, changeset, fn {field, error}, acc ->
add_error(acc, field, error)
end)
end
else
changeset
end
end
And I have two discount types:
defmodule SG.Shop.Discounts.DiscountType.Package do
use SG.Shop.Discounts.DiscountType
alias SG.Shop.Discounts.Discount
alias SG.Shop.Events.ProductType
@name DiscountType.name(__MODULE__)
defmodule Params do
use DiscountParams
@primary_key false
embedded_schema do
field :product_type_a, :string
field :product_type_b, :string
end
@required_params ~w(product_type_a product_type_b)a
@optional_params ~w()a
def changeset(discount_params, params \ %{}) do
discount_params
|> cast(params, @required_params ++ @optional_params)
|> validate_required(@required_params)
|> validate_inclusion(:product_type_a, ProductType.all_types())
|> validate_inclusion(:product_type_b, ProductType.all_types())
end
end
end
defmodule SG.Shop.Discounts.DiscountType.Package do
use SG.Shop.Discounts.DiscountType
alias SG.Shop.Discounts.Discount
alias SG.Shop.Events.ProductType
@name DiscountType.name(__MODULE__)
defmodule Params do
use DiscountParams
@primary_key false
embedded_schema do
field :product_type_a, :string
field :product_type_b, :string
end
@required_params ~w(product_type_a product_type_b)a
@optional_params ~w()a
def changeset(discount_params, params \ %{}) do
discount_params
|> cast(params, @required_params ++ @optional_params)
|> validate_required(@required_params)
|> validate_inclusion(:product_type_a, ProductType.all_types())
|> validate_inclusion(:product_type_b, ProductType.all_types())
end
end
end
I have tried the above approach, but I am facing issues with dynamically setting the schema_module. How can I achieve this so the validation goes through the embedded schema based on the type we get? And discounts discount_type is created only with this fields not with all given from params
defmodule Discounts.DiscountType.Volume do
defmodule Params do
use DiscountParams
@primary_key false
embedded_schema do
field(:n, :integer)
field(:product_type, :string)
end
@required_params ~w(n product_type)a
@optional_params ~w()a
def changeset(discount_params, params \ %{}) do
discount_params
|> cast(params, @required_params ++ @optional_params)
|> validate_required(@required_params)
|> validate_inclusion(:product_type, ProductType.all_types())
|> validate_number(:n, greater_than: 0, less_than_or_equal_to: 100)
end
end
end