Skip to content

Instantly share code, notes, and snippets.

@pikender
Last active May 5, 2016 12:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pikender/cf679e63f915fc780903496f3a36dfa2 to your computer and use it in GitHub Desktop.
Save pikender/cf679e63f915fc780903496f3a36dfa2 to your computer and use it in GitHub Desktop.
Change Ecto#schema API to Ecto#extend_schema for easy addition and removal from outside using meta-programming
defmodule Nectar.ModelExtension do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :schema_changes, accumulate: true)
Module.register_attribute(__MODULE__, :remove_schema_changes, accumulate: true)
import Nectar.ModelExtension, only: [add_to_schema: 1, remove_from_schema: 1]
@before_compile Nectar.ModelExtension
end
end
defmacro add_to_schema([do: block]) do
schema_change = Macro.escape(block)
quote bind_quoted: [schema_change: schema_change] do
Module.put_attribute(__MODULE__, :schema_changes, schema_change)
end
end
defmacro remove_from_schema([do: block]) do
to_remove = Macro.escape(block)
quote bind_quoted: [to_remove: to_remove] do
Module.put_attribute(__MODULE__, :remove_schema_changes, to_remove)
end
end
defmacro __before_compile__(_env) do
quote do
defmacro extendable_schema(source, [do: orig_schema]) do
final_schema = __process_schema_changes__(orig_schema)
quote do
# Create the Ecto#schema definition here
schema unquote(source) do
unquote(final_schema)
end
end
end
# rewrite the schema declaration
# and pass AST
def __process_schema_changes__({:__block__, opts, declarations}) do
to_remove = @remove_schema_changes
to_add = @schema_changes
updated_declarations = Enum.filter(declarations, fn ({declaration_type, _declarataion_opt, declaration_params}) ->
# return false if found in to_remove list
!Enum.any?(to_remove, fn ({type, _opt, params}) ->
type == declaration_type && params == declaration_params
end)
end) ++ to_add # append the fields to add at the end of declaration list
{:__block__, opts, updated_declarations}
end
end
end
end
defmodule Nectar.ExtendProduct do
use Nectar.ModelExtension
add_to_schema do: (field :special, :boolean, virtual: true)
add_to_schema do: (field :type, :string, virtual: true)
remove_from_schema do: (field :slug, :string)
# must exactly match the declaration, no fuzzy matching supported
remove_from_schema do: (field :remove_me, :string, virtual: true)
end
defmodule Nectar.Product do
import Nectar.ExtendProduct
use Nectar.Web, :model
use Arc.Ecto.Model
# Note the use of *extendable_schema* instead of schema
extendable_schema "products" do
field :name, :string
field :description, :string
field :available_on, Ecto.Date
field :discontinue_on, Ecto.Date
# let's remove these two
field :slug, :string
field :remove_me, :string, virtual: true
has_one :master, Nectar.Variant, on_delete: :nilify_all # As this and below association same, how to handle on_delete
has_many :variants, Nectar.Variant, on_delete: :nilify_all
has_many :product_option_types, Nectar.ProductOptionType
has_many :option_types, through: [:product_option_types, :option_type]
has_many :product_categories, Nectar.ProductCategory
has_many :categories, through: [:product_categories, :category]
timestamps
end
@required_fields ~w(name description available_on)
@optional_fields ~w(slug special)
@doc """
Creates a changeset based on the `model` and `params`.
If no params are provided, an invalid changeset is returned
with no validation performed.
"""
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
end
def create_changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> Validations.Date.validate_not_past_date(:available_on)
|> Nectar.Slug.generate_slug()
|> cast_assoc(:master, required: true, with: &Nectar.Variant.create_master_changeset/2)
|> cast_assoc(:product_option_types, required: true, with: &Nectar.ProductOptionType.from_product_changeset/2)
|> cast_assoc(:product_categories, with: &Nectar.ProductCategory.from_product_changeset/2)
|> unique_constraint(:slug)
end
def update_changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> Validations.Date.validate_not_past_date(:available_on)
|> Nectar.Slug.generate_slug()
|> cast_assoc(:product_categories, with: &Nectar.ProductCategory.from_product_changeset/2)
|> cast_assoc(:master, required: true, with: &Nectar.Variant.update_master_changeset/2)
|> validate_available_on_lt_discontinue_on
|> cast_assoc(:product_option_types, required: true, with: &Nectar.ProductOptionType.from_product_changeset/2)
|> unique_constraint(:slug)
end
defp validate_available_on_lt_discontinue_on(changeset) do
changed_master = get_change(changeset, :master)
changed_discontinue_on = if changed_master do
get_change(changed_master, :discontinue_on) || changed_master.model.discontinue_on
else
changeset.model.master.discontinue_on
end
changeset
|> Validations.Date.validate_lt_date(:available_on, changed_discontinue_on)
end
def has_variants_excluding_master?(product) do
Nectar.Repo.one(from variant in all_variants(product), select: count(variant.id)) > 0
end
def variant_count(product) do
Nectar.Repo.one(from variant in all_variants_including_master(product), select: count(variant.id))
end
def master_variant(model) do
from variant in all_variants_including_master(model), where: variant.is_master
end
def all_variants(model) do
from variant in all_variants_including_master(model), where: not(variant.is_master)
end
def all_variants_including_master(model) do
from variant in assoc(model, :variants)
end
# helper queries for preloading variant data.
@master_query from m in Nectar.Variant, where: m.is_master
@variant_query from m in Nectar.Variant, where: not(m.is_master), preload: [option_values: :option_type]
def products_with_master_variant do
from p in Nectar.Product, preload: [master: ^@master_query]
end
def products_with_variants do
from p in Nectar.Product, preload: [master: ^@master_query, variants: ^@variant_query]
end
def product_with_master_variant(product_id) do
from p in Nectar.Product,
where: p.id == ^product_id,
preload: [master: ^@master_query]
end
def product_with_variants(product_id) do
from p in Nectar.Product,
where: p.id == ^product_id,
preload: [variants: ^@variant_query, master: ^@master_query]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment