Packages

exocci provides libs and DSL for designing and manipulating OCCI (meta-)models.

Current section

Files

Jump to
occi lib occi model.ex
Raw

lib/occi/model.ex

defmodule OCCI.Model do
@moduledoc """
Use this module to define categories for your application.
Defines the following functions for manipulating categories:
* `kind?/1`: check a kind is part of this model
* `kinds/0`: returns list of supported kinds, including imported ones
* `mixin?/1`: check a mixin is part of this model
* `mixins/0`: returns list of availables mixins
* `mixin/1`: add a user mixin
* `del_mixin/1`: delete a user mixin
Available macros:
* `extends/1`: import categories from another model
* `kind/3`: defines a new kind
* `mixin/2`: defines a new mixin
"""
alias OCCI.OrdSet
alias OCCI.Category.Helpers
@doc false
defmacro __using__(opts) do
import_core = Keyword.get(opts, :core, true)
scheme =
case Keyword.get(opts, :scheme) do
nil -> raise "Using OCCI.Model requires :scheme arg"
s -> Helpers.__valid_scheme__(s)
end
Module.put_attribute(__CALLER__.module, :scheme, scheme)
user_mixins_mod = Module.concat(__CALLER__.module, UserMixins)
Module.put_attribute(__CALLER__.module, :imports, OrdSet.new())
Module.put_attribute(__CALLER__.module, :kinds, Map.new())
Module.put_attribute(__CALLER__.module, :mixins, Map.new())
Module.put_attribute(__CALLER__.module, :actions, [])
quote do
unquote(prelude())
import_core = unquote(import_core)
unquote(maybe_import_core(import_core))
user_mixins_mod = unquote(user_mixins_mod)
unquote(define_user_mixins_mod(user_mixins_mod))
unquote(define_user_mixins_functions(user_mixins_mod))
unquote(define_common_functions())
@doc """
Return module associated with the given category
"""
@spec module(charlist() | String.t() | atom) :: atom
def module(name) do
name = :"#{name}"
# credo:disable-for-lines:7
Map.get_lazy(@kinds, name, fn ->
Map.get_lazy(@mixins, name, fn ->
Map.get_lazy(unquote(user_mixins_mod).__mixins__(), name, fn ->
Enum.find_value(@imports, & &1.module(name))
end)
end)
end)
end
unquote(define_hidden_functions())
end
end
@doc false
defmacro __before_compile__(env) do
# Generate documentation
{line, doc} =
case Module.get_attribute(env.module, :moduledoc) do
nil -> {2, ""}
{line, doc} -> {line, doc}
end
doc =
doc
|> gen_exts_doc(Module.get_attribute(env.module, :imports))
|> gen_kinds_doc(Module.get_attribute(env.module, :kinds))
|> gen_mixins_doc(Module.get_attribute(env.module, :mixins))
Module.put_attribute(env.module, :moduledoc, {line, doc})
end
@doc """
Import categories from a model.
A model is represented by an elixir module.
OCCI.Model.Core is imported by default in all models.
"""
defmacro extends(name) do
mod = Macro.expand(name, __CALLER__)
imports = Module.get_attribute(__CALLER__.module, :imports)
Module.put_attribute(__CALLER__.module, :imports, OrdSet.add(imports, mod))
end
@doc """
Defines a new kind
Args:
* parent: category or module kind's parent
* scheme: if different from model's scheme
* term: if different from lower-case module name
* title: kind's description
"""
defmacro kind({:__aliases__, _, name}, args \\ [], do_block \\ nil) do
model = __CALLER__.module
term =
case Keyword.get(args, :term) do
nil ->
t = String.downcase(Enum.join(name, ""))
:"#{t}"
t ->
:"#{t}"
end
scheme =
case Keyword.get(args, :scheme) do
nil -> Module.get_attribute(model, :scheme)
s -> Helpers.__valid_scheme__(s)
end
modname = Helpers.__merge_modules__(model, name)
parent =
case Keyword.get(args, :parent) do
{:__aliases__, _, _} = aliases -> Macro.expand(aliases, __CALLER__)
nil -> nil
p -> model.__mod__(p)
end
args = [
{:model, model},
{:parent, parent},
{:scheme, scheme},
{:term, term}
| args
]
kinds = Module.get_attribute(model, :kinds)
Module.put_attribute(
model,
:kinds,
Map.put(kinds, :"#{scheme}##{term}", Macro.expand(modname, __CALLER__))
)
do_block = do_block || Keyword.get(args, :do)
quote do
defmodule unquote(modname) do
use OCCI.Kind, unquote(args)
unquote(do_block)
end
end
end
@doc """
Defines a new mixin
"""
defmacro mixin({:__aliases__, _, name}, args \\ [], do_block \\ nil) do
model = __CALLER__.module
term =
case Keyword.get(args, :term) do
nil ->
t = String.downcase(Enum.join(name, ""))
:"#{t}"
t ->
:"#{t}"
end
scheme =
case Keyword.get(args, :scheme) do
nil -> Module.get_attribute(model, :scheme)
s -> Helpers.__valid_scheme__(s)
end
modname = Helpers.__merge_modules__(model, name)
args = [
{:model, model},
{:depends, Keyword.get(args, :depends, [])},
{:applies, Keyword.get(args, :applies, [])},
{:scheme, scheme},
{:term, term}
| args
]
do_block = do_block || Keyword.get(args, :do)
mixins = Module.get_attribute(model, :mixins)
Module.put_attribute(
model,
:mixins,
Map.put(mixins, :"#{scheme}##{term}", Macro.expand(modname, __CALLER__))
)
quote do
defmodule unquote(modname) do
use OCCI.Mixin, unquote(args)
unquote(do_block)
end
end
end
###
### Priv
###
defp prelude do
quote do
require OCCI.Model
import OCCI.Model
@before_compile unquote(__MODULE__)
end
end
def maybe_import_core(false), do: nil
def maybe_import_core(true) do
quote do
imports = Module.get_attribute(__MODULE__, :imports)
Module.put_attribute(__MODULE__, :imports, OrdSet.add(imports, OCCI.Model.Core))
end
end
def define_user_mixins_mod(name) do
quote do
defmodule unquote(name) do
@doc false
def mixins, do: []
@doc false
def __mixins__, do: Map.new()
end
end
end
def define_common_functions do
quote do
@doc """
Return true if `name` is a valid kind module
"""
@spec kind?(atom) :: boolean
def kind?(mod) when is_atom(mod) do
function_exported?(mod, :__occi_type__, 0) and mod.__occi_type__() == :kind
end
@doc """
Return true if `name` if a valid mixin module
"""
@spec mixin?(atom) :: boolean
def mixin?(mod) when is_atom(mod) do
function_exported?(mod, :__occi_type__, 0) and mod.__occi_type__() == :mixin
end
@doc """
Given a list of categories, returns all attributes specs
"""
def specs(categories) do
Enum.reduce(categories, OrdSet.new(), fn category, acc ->
OrdSet.merge(acc, category.__specs__())
end)
end
@doc """
Given a list of categories, returns list of required attributes
"""
def required(categories) do
Enum.reduce(categories, OrdSet.new(), fn category, acc ->
OrdSet.merge(acc, category.required())
end)
end
@doc """
Given a list of categories, returns all action specifications
"""
def actions(categories) do
Enum.reduce(categories, OrdSet.new(), fn category, acc ->
OrdSet.merge(acc, category.actions())
end)
end
end
end
def define_user_mixins_functions(user_mixins_mod) do
quote do
@doc """
Return list of available mixins
"""
@spec mixins() :: [atom]
def mixins do
Enum.reduce(
@imports,
OrdSet.merge(Map.values(@mixins), unquote(user_mixins_mod).mixins()),
&OrdSet.merge(&1.mixins(), &2)
)
end
@doc """
Add user mixin (tag)
* `module`: module name, related to model name
* `category`: category name
"""
@spec add_mixin(module :: atom, category :: charlist() | String.t() | atom) :: atom
def add_mixin(module, category) do
module = Module.concat(Module.split(__MODULE__) ++ Module.split(module))
{scheme, term} = Helpers.__parse_category__(category)
args = [{:model, __MODULE__}, {:scheme, scheme}, {:term, term}, {:tag, true}]
q =
quote do
defmodule unquote(module) do
use OCCI.Mixin, unquote(args)
end
end
_ = Code.compile_quoted(q)
:ok =
update_user_mixins(
Map.put(unquote(user_mixins_mod).__mixins__(), :"#{category}", module)
)
module
end
@doc """
Delete user mixin (tag)
"""
@spec del_mixin(module :: atom) :: :ok | :error
def del_mixin(module) do
if function_exported?(module, :__occi_type__, 0) and module.__occi_type__() == :mixin and
module.tag?() do
category = module.category()
true = :code.delete(module)
:ok = update_user_mixins(unquote(user_mixins_mod).__mixins__() |> Map.delete(category))
else
:error
end
end
@doc """
Returns list of mixins applicable to a given kind
"""
def applicable_mixins(kind) do
Enum.reduce(mixins(), [], fn mixin, acc ->
if mixin.apply?(kind), do: [mixin | acc], else: acc
end)
end
defp update_user_mixins(mixins) do
mixins = Macro.escape(mixins)
mod = unquote(user_mixins_mod)
q =
quote do
defmodule unquote(mod) do
@mixins unquote(mixins)
@doc """
Return list of user mixins
"""
def mixins, do: Map.values(@mixins)
@doc false
def __mixins__, do: @mixins
end
end
_ = Code.compiler_options(ignore_module_conflict: true)
_ = Code.compile_quoted(q)
:ok
end
end
end
defp define_hidden_functions do
quote do
@doc false
def __imports__ do
Enum.reduce(@imports, OCCI.OrdSet.new(), &OCCI.OrdSet.merge(&2, [&1 | &1.__imports__()]))
end
@doc false
#
# When launching an action on an entity, look for implementation in
# creation model, creation model's imports, then related category.
#
def __exec__(action, entity) do
fun = :"__#{OCCI.Action.id(action)}__"
attrs = OCCI.Action.attributes(action)
related = OCCI.Action.related(action)
related_mod = OCCI.Action.__related_mod__(action)
model = OCCI.Model.Core.Entity.__model__(entity)
__exec__(entity, attrs, related, related_mod, fun, [model | model.__imports__()])
end
defp __exec__(entity, attrs, _related, related_mod, fun, []) do
try do
apply(related_mod, fun, [entity, attrs])
rescue
UndefinedFunctionError ->
# No implementation found, returns entity
entity
end
end
defp __exec__(entity, attrs, related, related_mod, fun, [model | models]) do
try do
apply(model, fun, [entity, attrs])
rescue
e in UndefinedFunctionError ->
__exec__(entity, attrs, related, related_mod, fun, models)
end
end
end
end
defp gen_exts_doc(doc, exts) do
if Enum.empty?(exts) do
doc
else
doc =
doc <>
"""
Imported extensions:
"""
Enum.reduce(exts, doc, fn ext, acc ->
acc <>
"""
* `#{ext}`
"""
end)
end
end
defp gen_kinds_doc(doc, kinds) do
if Enum.empty?(kinds) do
doc
else
doc =
doc <>
"""
Defined Kinds:
"""
Enum.reduce(kinds, doc, fn {kind, _}, acc ->
acc <>
"""
* _#{kind}_
"""
end)
end
end
defp gen_mixins_doc(doc, mixins) do
if Enum.empty?(mixins) do
doc
else
doc =
doc <>
"""
Defined Mixins:
"""
Enum.reduce(mixins, doc, fn {mixin, _}, acc ->
acc <>
"""
* _#{mixin}_
"""
end)
end
end
end