Packages

plymio_codi: Generating Quoted Forms for Common Code Patterns

Current section

Files

Jump to
plymio_codi lib codi pattern delegate delegate.ex
Raw

lib/codi/pattern/delegate/delegate.ex

defmodule Plymio.Codi.Pattern.Delegate do
@moduledoc ~S"""
The *delegate* patterns build `Kernel.defdelegate/2` call(s).
Delegated functions can be built with, optionally, with a `@doc`,
`@since` and/or `@spec`.
See `Plymio.Codi` for an overview and documentation terms
Note the delegated mfa: `{module, function, arity}` is validated
i.e. the `function` must exist in the `module` with the given
`arity`.
If `:delegate_doc` is not in the pattern opts, a default of
`:delegate` is used. (It can be disabled by explicily setting
`:fun_doc` to `nil` - **not** `false`).
## Pattern: *delegate*
Valid keys in the *cpo* are:
| Key | Aliases |
| :--- | :--- |
| `:delegate_module` | *:to, :module, :fun_mod, :fun_module, :function_module* |
| `:delegate_name` | *:as* |
| `:delegate_doc` | *:doc, :fun_doc, :function_doc* |
| `:delegate_args` | *:args, :fun_args, :function_args* |
| `:delegate_arity` | *:arity, :fun_arity, :function_arity* |
| `:fun_name` | *:name, :function_name* |
| `:spec_args` | |
| `:spec_result` |*:result, :fun_result, :function_result* |
| `:since` | |
## Examples
A simple case. Note the automatically generated `:delegate`-format `@doc`.
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_one, arity: 1, module: ModuleA],
...> delegate: [name: :fun_due, arity: 2, module: ModuleA],
...> delegate: [name: :fun_tre, arity: 3, module: ModuleA]
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["@doc \"Delegated to `ModuleA.fun_one/1`\"",
"defdelegate(fun_one(var1), to: ModuleA)",
"@doc \"Delegated to `ModuleA.fun_due/2`\"",
"defdelegate(fun_due(var1, var2), to: ModuleA)",
"@doc \"Delegated to `ModuleA.fun_tre/3`\"",
"defdelegate(fun_tre(var1, var2, var3), to: ModuleA)"]
Here showing the auto-generated `@doc` disabled.
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_one, arity: 1, module: ModuleA, doc: nil],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["defdelegate(fun_one(var1), to: ModuleA)"]
This example shows explicit function arguments (`:args`) being given:
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_one, args: :opts, module: ModuleA, doc: nil],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["defdelegate(fun_one(opts), to: ModuleA)"]
Delegating to a different function name (`:as`):
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_3, as: :fun_tre, args: [:opts, :key, :value], module: ModuleA],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["@doc \"Delegated to `ModuleA.fun_tre/3`\"",
"defdelegate(fun_3(opts, key, value), to: ModuleA, as: :fun_tre)"]
Here a `@doc`, `@since`, and `@spec` are generated. Note in the first
example the `:spec_args` are explicily given as well as the
`:spec_result`. In the second no `:spec_args` are given and the
arity used.
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_one, arity: 1, module: ModuleA,
...> since: "1.7.9", spec_args: :integer, spec_result: :tuple],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["@doc \"Delegated to `ModuleA.fun_one/1`\"",
"@since \"1.7.9\"",
"@spec fun_one(integer) :: tuple",
"defdelegate(fun_one(var1), to: ModuleA)"]
iex> {:ok, {forms, _}} = [
...> delegate: [name: :fun_one, arity: 1, module: ModuleA,
...> since: "1.7.9", spec_result: :tuple],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["@doc \"Delegated to `ModuleA.fun_one/1`\"",
"@since \"1.7.9\"",
"@spec fun_one(any) :: tuple",
"defdelegate(fun_one(var1), to: ModuleA)"]
Showing validation of the `mfa`:
iex> {:error, error} = [
...> delegate: [name: :fun_one, arity: 2, module: ModuleZ],
...> ] |> produce_codi
...> error |> Exception.message
"mfa {ModuleZ, :fun_one, 2} module unknown"
iex> {:error, error} = [
...> delegate: [name: :fun_1, arity: 2, module: ModuleA],
...> ] |> produce_codi
...> error |> Exception.message
"mfa {ModuleA, :fun_1, 2} function unknown"
iex> {:error, error} = [
...> delegate: [name: :fun_one, arity: 2, module: ModuleA],
...> ] |> produce_codi
...> error |> Exception.message
"mfa {ModuleA, :fun_one, 2} arity unknown"
## Pattern: *delegate_module*
The *delegate_module* pattern builds a delegate function
for one or more functions in a module.
As with `:delegate`, `@doc` and/or `@since` can be generated at the same time.
Valid keys in the *cpo* are:
| Key | Aliases |
| :--- | :--- |
| `:delegate_module` | *:to :module, :fun_module, :fun_mod, :function_module* |
| `:delegate_doc` | *:doc, :fun_doc, :function_doc* |
| `:take` | |
| `:drop` | |
| `:filter` | |
| `:reject` | |
| `:since` | |
To determine which functions to delegate, the "function v arity"
(*fva*) for the module is first obtained by calling e.g. `ModuleA.__info__(:functions)`.
The *delegate options* can include `:take`, `:drop`, `:filter` or
`:reject` keys to "edit" the *fva*..
The first two take zero, one or more function names
and are used in a call to e.g. `Keyword.take/2` with the *fva*.
The second two keys require an arity 1 function (predicate) passed a
`{fun,arity}` tuple, returning `true` or `false` and is used with e.g. `Enum.filter/2`.
> Note the fva edits are applied in order of occurence so `:take`-ing a function already `:reject`-ed will do nothing.
Here all functions in the module (`ModuleA`) are wanted with auto-generated `@doc` and `@since`:
iex> {:ok, {forms, _}} = [
...> delegate_module: [module: ModuleA, since: "1.7.9"],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["@doc \"Delegated to `ModuleA.fun_due/2`\"",
"@since \"1.7.9\"",
"defdelegate(fun_due(var1, var2), to: ModuleA)",
"@doc \"Delegated to `ModuleA.fun_one/1`\"",
"@since \"1.7.9\"",
"defdelegate(fun_one(var1), to: ModuleA)",
"@doc \"Delegated to `ModuleA.fun_tre/3`\"",
"@since \"1.7.9\"",
"defdelegate(fun_tre(var1, var2, var3), to: ModuleA)"]
Here arity 2 funs are selected, and `@doc` is disabled.
iex> {:ok, {forms, _}} = [
...> delegate_module: [
...> module: ModuleA, doc: nil,
...> filter: fn {_fun,arity} -> arity == 3 end],
...> ] |> produce_codi
...> forms |> harnais_helper_format_forms!
["defdelegate(fun_tre(var1, var2, var3), to: ModuleA)"]
"""
alias Plymio.Codi, as: CODI
use Plymio.Fontais.Attribute
use Plymio.Codi.Attribute
import Plymio.Fontais.Option,
only: [
opts_canonical_keys: 2,
opts_take_canonical_keys: 2,
opts_create_aliases_dict: 1
]
import Plymio.Codi.Utility,
only: [
cpo_resolve_fun_name: 1,
cpo_resolve_delegate_module: 1,
cpo_resolve_delegate_name: 1,
cpo_resolve_delegate_doc: 1,
cpo_resolve_delegate_args: 1
]
import Plymio.Codi.Utility.Module,
only: [
reduce_module_fva: 2,
state_validate_mfa: 2,
state_resolve_module_fva: 2
]
import Plymio.Funcio.Enum.Map.Collate,
only: [
map_collate0_enum: 2
]
import Plymio.Codi.CPO
@pattern_delegate_kvs_alias [
@plymio_codi_key_alias_pattern,
@plymio_codi_key_alias_status,
@plymio_codi_key_alias_form,
@plymio_codi_key_alias_since,
@plymio_codi_key_alias_fun_name,
@plymio_codi_key_alias_delegate_doc,
@plymio_codi_key_alias_delegate_module,
@plymio_codi_key_alias_delegate_name,
@plymio_codi_key_alias_delegate_args,
@plymio_codi_key_alias_delegate_arity,
# limited aliases
{@plymio_codi_key_typespec_spec_args, [:spec_args]},
@plymio_codi_key_alias_typespec_spec_result,
@plymio_codi_key_alias_forms_edit
]
@pattern_delegate_dict_alias @pattern_delegate_kvs_alias
|> opts_create_aliases_dict
@doc false
def cpo_pattern_delegate_normalise(opts, dict \\ nil) do
opts |> opts_take_canonical_keys(dict || @pattern_delegate_dict_alias)
end
@pattern_delegate_module_kvs_alias [
@plymio_codi_key_alias_pattern,
@plymio_codi_key_alias_status,
@plymio_codi_key_alias_form,
@plymio_codi_key_alias_since,
@plymio_codi_key_alias_delegate_module,
@plymio_codi_key_alias_delegate_doc,
{@plymio_codi_key_take, nil},
{@plymio_codi_key_drop, nil},
{@plymio_codi_key_filter, nil},
{@plymio_codi_key_reject, nil},
@plymio_codi_key_alias_forms_edit
]
@pattern_delegate_module_dict_alias @pattern_delegate_module_kvs_alias
|> opts_create_aliases_dict
@doc false
def cpo_pattern_delegate_module_normalise(opts, dict \\ nil) do
opts |> opts_canonical_keys(dict || @pattern_delegate_module_dict_alias)
end
@doc false
def express_pattern(%CODI{} = state, pattern, cpo)
when pattern == @plymio_codi_pattern_delegate do
with {:ok, cpo} <- cpo |> cpo_pattern_delegate_normalise,
{:ok, fun_name} <- cpo |> cpo_resolve_fun_name,
{:ok, delegate_module} <- cpo |> cpo_resolve_delegate_module,
{:ok, cpo} <- cpo |> cpo_maybe_put_delegate_name(fun_name),
{:ok, delegate_name} <- cpo |> cpo_resolve_delegate_name,
{:ok, delegate_args} <- cpo |> cpo_resolve_delegate_args,
{:ok, cpo} <- cpo |> cpo_maybe_put_delegate_doc(@plymio_codi_doc_type_delegate),
{:ok, delegate_doc} <- cpo |> cpo_resolve_delegate_doc,
{:ok, {_, %CODI{} = state}} <-
state |> state_validate_mfa({delegate_module, delegate_name, length(delegate_args)}),
# base dependent cpo
{:ok, depend_cpo} <- cpo |> cpo_mark_status_active,
{:ok, depend_cpo} <- depend_cpo |> cpo_put_fun_module(delegate_module),
# the dependent doc cpo
{:ok, depend_doc_cpo} <- depend_cpo |> cpo_put_pattern(@plymio_codi_pattern_doc),
{:ok, depend_doc_cpo} <- depend_doc_cpo |> cpo_put_fun_name(delegate_name),
{:ok, depend_doc_cpo} <- depend_doc_cpo |> cpo_put_fun_doc(delegate_doc),
{:ok, depend_doc_cpo} <- depend_doc_cpo |> cpo_put_fun_args(delegate_args),
# the dependent since cpo
{:ok, depend_since_cpo} <- depend_cpo |> cpo_put_pattern(@plymio_codi_pattern_since),
# the dependent type cpo
{:ok, depend_type_cpo} <-
depend_cpo |> cpo_put_pattern(@plymio_codi_pattern_typespec_spec),
{:ok, depend_type_cpo} <-
depend_type_cpo
|> cpo_maybe_add_typespec_spec_opts([
{@plymio_codi_key_typespec_spec_arity, delegate_args |> length}
]),
true <- true do
delegate_opts =
(delegate_name == fun_name)
|> case do
true ->
[to: delegate_module]
_ ->
[to: delegate_module, as: delegate_name]
end
pattern_form =
quote do
defdelegate unquote(fun_name)(unquote_splicing(delegate_args)), unquote(delegate_opts)
end
depend_patterns = [
depend_doc_cpo,
depend_since_cpo,
depend_type_cpo
]
with {:ok, %CODI{} = depend_state} <- state |> CODI.update_snippets(depend_patterns),
{:ok, {depend_product, %CODI{}}} <-
depend_state |> Plymio.Codi.Stage.Normalise.normalise_snippets(),
{:ok, depend_cpos} <- depend_product |> cpo_fetch_patterns,
{:ok, cpo} <- cpo |> cpo_done_with_edited_form(pattern_form) do
cpos = depend_cpos ++ [cpo]
{:ok, {cpos, state}}
else
{:error, %{__exception__: true}} = result -> result
end
else
{:error, %{__exception__: true}} = result -> result
end
end
def express_pattern(%CODI{} = state, pattern, opts)
when pattern == @plymio_codi_pattern_delegate_module do
with {:ok, opts} <- opts |> cpo_pattern_delegate_module_normalise,
{:ok, delegate_module} <- opts |> cpo_fetch_delegate_module,
{:ok, {delegate_fva, %CODI{} = state}} <-
state |> state_resolve_module_fva(delegate_module),
{:ok, delegate_fva} <- delegate_fva |> reduce_module_fva(opts),
{:ok, delegate_cpo} <- opts |> cpo_pattern_delegate_normalise,
{:ok, delegate_cpo} <- delegate_cpo |> cpo_mark_status_active,
{:ok, delegate_cpo} <- delegate_cpo |> cpo_put_pattern(@plymio_codi_pattern_delegate) do
delegate_fva
|> map_collate0_enum(fn {name, arity} ->
with {:ok, cpo} <- delegate_cpo |> cpo_put_fun_name(name),
{:ok, cpo} <- cpo |> cpo_put_delegate_name(name),
{:ok, _cpo} = result <- cpo |> cpo_put_delegate_arity(arity) do
result
else
{:error, %{__exception__: true}} = result -> result
end
end)
|> case do
{:error, %{__struct__: _}} = result -> result
{:ok, cpos} -> {:ok, {cpos, state}}
end
else
{:error, %{__exception__: true}} = result -> result
end
end
end