Last active
May 5, 2016 12:02
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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