Current section
Files
Jump to
Current section
Files
lib/schatten/schatten.ex
defmodule Harnais.Form.Schatten do
@moduledoc ~S"""
A specialised harness for testing Quoted Forms.
This module does the "heavy lift" for
`Harnais.Form.harnais_form_test_forms/2` but can be used stand-alone
by calling `produce_schatten/2` directly.
See `Harnais.Form` for overview and other documentation terms.
## Documentation Terms
### *schatten*
An instance of the module's `struct`.
## Production Overview
`produce_schatten/2` runs a workflow of dependent phases with each phase assocated with a
field (e.g. *forms*, *text*, *format text*) and each phase having
one or more stages identified by a *verb* (e.g. `build`, `express`, `actual`, `expect`).
To explain, consider an example where a *forms* is passed to
`produce_schatten/2` and needs to produce the code formatted
by the Elixir code formatter.
To illustrate, here are the example forms:
[quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]
First the *schatten* must `build` a single *form* from the *forms*
using `Kernel.unquote_splicing/1`. (The single *form* is a block (`:__block__`) and
showing it here just clutters the explanation.)
Next it must `build` the *text* from the single *form* using `Macro.to_text/1`.
"(\n def(add1(x) do\n x + 1\n end)\n def(sub1(x) do\n x - 1\n end)\n)"
Then the *text* must be used to `build` the *format text* using `Code.format_string!/1` (and then joining the iodata):
"def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)"
Finally the *schatten* must `express` the *format text* into the *product* as e.g.
[format_text: "def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)"]
Here is the doctest that shows the above explanation in action:
iex> forms = [quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]
...> {:ok, {[format_text: format_text], _schatten}} = forms
...> |> produce_schatten(express: :format_text)
...> format_text
"def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)"
This silly example shows the use of a function to `:express` the *format text* - here it uppercases it:
iex> forms = [quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]
...> {:ok, {[format_text: format_text], _schatten}} = forms
...> |> produce_schatten(express: [
...> {:format_text, fn {:actual,:format_text,format_text} -> {:ok, format_text |> String.upcase} end}])
...> format_text
"DEF(\n ADD1(X) DO\n X + 1\n END\n)\n\nDEF(\n SUB1(X) DO\n X - 1\n END\n)"
If wanted, the input *forms* could be `express`-ed also so the *product* would contain:
[format_text: "def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)",
forms: [quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]]
Production also allows the actual value of a field to be
compared with an expected value using the `:expect` verb. This
variant of the doctest above just confirms the *format text* is as expected
but does not bother to `express` anything (so the product is empty).
iex> forms = [quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]
...> {:ok, {product, _schatten}} = forms
...> |> produce_schatten(expect: [
...> format_text: "def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)"])
...> product
[]
If the `:expect`-ed value does not match to `:actual` value, an
error result will be returned. This doctest exports
(`Harnais.Error.export_exception/2`) the error to show the details of the error.
iex> forms = [quote(do: def add1(x, do: x + 1)), quote(do: def sub1(x, do: x - 1))]
...> {:error, error} = forms
...> |> produce_schatten(expect: [format_text: "this is wrong"])
...> error |> Harnais.Error.export_exception
{:ok, [error: [[m: "compare expect actual failed",
r: :mismatch,
t: :value,
l: :format_text,
v1: "def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)",
v2: "this is wrong"]]]}
## Production Fields
`produce_schatten/2` has these *standard fields* that it knows how to derive from the *form/forms*:
| Field | Purpose |
| :--- | :--- |
| `result` | *the first element in the 2tuple returned by `Code.eval_quoted/3`* |
| `error` | *the error - Exception is binary* |
| `form` | *the form or forms as a single form* |
| `forms` | *the form or forms as a list of forms* |
| `text` | *the string from calling `Macro.to_string/1` on the single form* |
| `texts` | *the list of strings from calling `Macro.to_string/1` on each form* |
| `format_text` | *the string from calling `Code.format_string!/1` on the *text* |
| `format_texts` | *the list of strings from calling `Code.format_string!/1` on each *text* |
## Production Workflow
Production is a simple workflow represented by a pipeline of one or
more 3tuples where the first element is the *verb* (e.g. `:actual`, `:build`,
`:express`), the second the *field* (e.g. *format text*) , and the third a value whose
meaning is specific to to verb/field combination.
When the workflow is run, each 3tuple usually adds one or more 3tuples to the end of the pipeline. For example
{:build, :format_text, build_fun}
finds the last 3tuple with `{:actual, :text, text}` e.g.
{:actual, :text, "(\n def(add1(x) do\n x + 1\n end)\n def(sub1(x) do\n x - 1\n end)\n)"}
passes the *text* to `Code.format_string!/1`, and then adds the
*format_text* 3tuple to the end of the pipeline:
{:actual, :format_text, "def(\n add1(x) do\n x + 1\n end\n)\n\ndef(\n sub1(x) do\n x - 1\n end\n)"}
Similary running `{:express, :format_text, express_fun}` finds the last
`{:actual, :format_text, format_text}` and adds `{:produce, :format_text, format_text}`
to the end of the pipeline.
Some verbs are generated automatically: for example `:express`-ing the
*text* field generates an automatic *build* for *text* (which in
turn generates a *build* for *form*).
The last instance of a 3tuple for each verb/field combination takes
precedence.
### Production Workflow 3Tuple: {:actual, field, value}
The `:actual` *verb* adds the `value` for the `field` to the pipeline as the literal 3tuple
`{:actual, field, value}`.
### Production Workflow 3Tuple : {:build, field, build_fun}
The `:build` *verb* finds the _last_ `:actual` value of the *field*
in the *pipeline* and calls the `build_fun`.
The `:build` 3tuple is a [Production Transform Tuple](#module-production-transform-tuples).
All of the *standard fields* have a default build function.
A `:build` can be (and usually is) generated automatically by
e.g. an `:expect` or `:express` for a field (see below).
### Production Workflow 3Tuple: {:express, field, express_fun}
The `:express` *verb* first finds the _last_ `{:actual, field, value}` 3tuple
in the *pipeline* and calls the `express_fun`.
The `:express` 3tuple is a [Production Transform Tuple](#module-production-transform-tuples).
### Production Workflow 3Tuple: {:expect, field, expect_value}
The `:expect` *verb* first finds the _last_ `{:actual, field, actual_value}` 3tuple
in the *pipeline*.
If the `expect_value` is a function, the 3tuple is treated as a [transform tuple](#module-product-transform-tuples)
*but* the return is normalised to *pattern 0 truthy result* (See `Harnais.Form.Utility.truthy0_result/1`).
Otherwise the `expect_value` is just compared (`Kernel.==/2`) with the `actual_value`.
If the compare does not return *truthy*, the workflow is aborted and `{:error, error}` returned.
## Production Product
The *product* is created from the *last* 3tuple for each `field`
where the `verb` is `:produce` and dropping the *verb*.
`:build` *verb* 3tuples are run before any other *verbs* and in
the order given.
## Production Transform Tuples
The value in the 3tuple for some *verbs* (e.g. `build`, `express`)
can be either *the unset value* or an arity 1 or 2 function.
When the value is *the unset value*, the default transform function
is used. For example, the default function for `:build`-ing the
*text* field from the *form* field calls `Macro.to_string/1`.
If the value is an arity 1 function, it is passed (usually) the
`:actual` tuple. (So `:build`-ing the *text* will be passed
`{:actual, :form, form}`.). The function must return either
e.g. `{:ok, {:actual, :text, text}}` or `{:ok, text}`.
If the value is an arity 2 function, it is passed the *schatten* and (usually) the
`:actual` tuple. The function must return either e.g. `{:ok, {:actual, :text, text}, schatten}`,
`{:ok, {:actual, :text, text}}` or `{:ok, text}`.
## Production Opts
The allowed keys in the production *opts* are:
### Production Opts key: `:transform_opts`
The forms, together with the value of `:transform_opts` are passed to
`harnais_form_transform_forms/2` and the transformed forms (re)added to the pipeline.
### Production Opts key: `:eval_binding`
The value of `:eval_binding` is used as the 2nd argument in the call to `Code.eval_quoted/3`. Default is an empty list.
### Production Opts Key: `:eval_opts`
The value of `:eval_opts` is used as the 3rd argument in the call to `Code.eval_quoted/3`. Default is `__ENV__`.
"""
require Plymio.Fontais.Option
use Plymio.Codi
alias Harnais.Form.Utility, as: HAU
alias Harnais.Form.Schatten.Workflow.Utility, as: HASWU
alias Harnais.Form.Schatten.Workflow.Depend, as: HASWD
alias Harnais.Form.Schatten.Workflow.Filter, as: HASWF
alias Harnais.Form.Schatten.Workflow.Edit, as: HASWE
alias Harnais.Form.Schatten.Workflow.Run, as: HASWR
use Harnais.Error.Attribute
use Harnais.Form.Attribute
use Harnais.Form.Attribute.Schatten
@codi_opts [
{@plymio_codi_key_vekil, Plymio.Vekil.Codi.__vekil__()}
]
import Harnais.Error,
only: [
new_error_result: 1
],
warn: false
import Plymio.Fontais.Guard,
only: [
is_value_set: 1,
is_filled_list: 1
]
import Plymio.Fontais.Option,
only: [
opts_validate: 1,
opts_create_aliases_dict: 1,
opts_canonical_keys: 2
]
import Plymio.Funcio.Enum.Reduce,
only: [
reduce0_enum: 3,
reduce2_enum: 3
]
@harnais_form_keys_opts_transform_ast_aliases HAU.opts_ast_transform_keys_aliases()
@type t :: %__MODULE__{}
@type form :: Harnais.ast()
@type forms :: Harnais.asts()
@type kv :: {any, any}
@type opts :: Harnais.opts()
@type error :: Harnais.error()
@harnais_form_schatten_kvs_aliases @harnais_form_aliases_opts_transform_ast ++
[
@harnais_form_schatten_field_alias_workflow_pipeline,
@harnais_form_schatten_field_alias_form,
@harnais_form_schatten_field_alias_eval_binding,
@harnais_form_schatten_field_alias_eval_result,
@harnais_form_schatten_field_alias_eval_opts,
@harnais_form_schatten_field_alias_transform_opts,
{@harnais_form_schatten_workflow_verb_build, []},
{@harnais_form_schatten_workflow_verb_actual, []},
{@harnais_form_schatten_workflow_verb_expect,
[:compare]},
{@harnais_form_schatten_workflow_verb_express, []},
{@harnais_form_schatten_workflow_verb_produce, []}
]
@harnais_form_schatten_dict_aliases @harnais_form_schatten_kvs_aliases
|> opts_create_aliases_dict
@doc false
def update_canonical_opts(opts, dict \\ @harnais_form_schatten_dict_aliases) do
opts |> opts_canonical_keys(dict)
end
@doc false
def opts_maybe_canonical_keys(opts, dict \\ @harnais_form_schatten_dict_aliases) do
opts |> opts_maybe_canonical_keys(dict)
end
@doc false
def opts_take_canonical_keys(opts, dict \\ @harnais_form_schatten_dict_aliases) do
with {:ok, opts} <- opts |> opts_maybe_canonical_keys(dict) do
{:ok, opts |> Keyword.take(dict |> Map.keys())}
else
{:error, %{__exception__: true}} = result -> result
end
end
@harnais_form_schatten_defstruct [
{@harnais_form_schatten_field_workflow_pipeline,
@harnais_form_schatten_workflow_pipeline_default},
{@harnais_form_schatten_field_form, @harnais_form_schatten_value_form_initial},
{@harnais_form_schatten_field_eval_binding,
@harnais_form_schatten_value_eval_binding_initial},
{@harnais_form_schatten_field_eval_result, @harnais_form_schatten_value_eval_result_initial},
{@harnais_form_schatten_field_eval_opts, @harnais_form_schatten_value_eval_opts_initial},
{@harnais_form_schatten_field_transform_opts,
@harnais_form_schatten_value_transform_opts_initial}
]
defstruct @harnais_form_schatten_defstruct
@doc ~S"""
`produce_schatten/2` expects its first argument to be a *forms*.
The second argument can either be a pre-created *schatten* or an *opts*
that will be passed to `new/1` to create a *schatten*.
It returns `{:ok, {product, %__MODULE__{} = schatten}}` or
`{:error, error}` where `product` will be a `Keyword`.
## Examples
The various ways the *verbs* can be combined makes for a large number of use cases. These are just a selection.
This example doesn't do anything; the product is empty.
iex> {:ok, {[], schatten}} = 42 |> produce_schatten
...> match?(%Harnais.Form.Schatten{}, schatten)
true
Here the expected form is tested but there are no *expressed* fields so again the product is empty.
iex> form = quote(do: x = x + 1)
...> {:ok, {[], schatten}} = form
...> |> produce_schatten(expect: [form: form])
...> match?(%Harnais.Form.Schatten{}, schatten)
true
To express the *form* in the *product*:
iex> form = quote(do: x = x + 1)
...> {:ok, {[form: ^form] = product, _schatten}} = form
...> |> produce_schatten(expect: [form: form], express: :form)
...> product
[form: quote(do: x = x + 1)]
Here a single form is expressed as a list of forms and texts:
iex> form = quote(do: x = x + 1)
...> {:ok, {[forms: [^form], texts: texts], _schatten}} = form
...> |> produce_schatten(expect: [form: form], express: [:forms, :texts])
...> texts
["x = x + 1"]
This example includes a `:binding` and `expect`s the `:result` to be 63.
iex> form = quote(do: x = x + 1)
...> {:ok, {[result: 8, text: text], _schatten}} = form
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: 8], express: [:result, :text])
...> text
"x = x + 1"
The expect value can be an arity 1 function that is passed the
actula value and should return a *pattern 0 truthy value* (See
`Harnais.Form.Utility.truthy0_result/1`). First example show the expect function returning *truthy*:
iex> form = quote(do: x = x + 1)
...> {:ok, {[result: 8, text: text], _schatten}} = form
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: fn _actual -> true end], express: [:result, :text])
...> text
"x = x + 1"
Second example shows the expect function returning `nil` (*falsy*):
iex> form = quote(do: x = x + 1)
...> {:error, error} = form
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: fn _actual -> nil end], express: [:result, :text])
...> error |> Exception.message
"compare expect actual function failed, got: field: :result, actual: 8"
Multiple forms and a *binding* make for a more interesting example:
iex> forms = [
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1),
...> ]
...> {:ok, {[result: 63, texts: texts], _schatten}} = forms
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: 63], express: [:result, :texts])
...> texts
["x = x + 1", "x = x * x", "x = x - 1"]
Or even multiple forms as a single form and as *text*:
iex> forms = [
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1),
...> ]
...> {:ok, {[result: 63, form: form], _schatten}} = forms
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: 63], express: [:result, :form])
...> form |> Macro.to_string
"(\n x = x + 1\n x = x * x\n x = x - 1\n)"
iex> forms = [
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1),
...> ]
...> {:ok, {[result: 63, text: text], _schatten}} = forms
...> |> produce_schatten(binding: [x: 7],
...> expect: [result: 63], express: [:result, :text])
...> text
"(\n x = x + 1\n x = x * x\n x = x - 1\n)"
The function supports transforming the *forms* before production.
The options supported by `Plymio.Fontais.Form.forms_edit/2` can
be given. In this example the forms are postwalked to change `x` to
`a`.
iex> forms = [
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1),
...> ]
...> {:ok, {[result: 63, text: text], _schatten}} = forms
...> |> produce_schatten(binding: [a: 7],
...> postwalk: fn
...> {:x, [], m} when is_atom(m) -> Macro.var(:a, m)
...> passthru -> passthru
...> end,
...> expect: [result: 63], express: [:result, :text])
...> text
"(\n a = a + 1\n a = a * a\n a = a - 1\n)"
The default `:build` function for the *standard fields* can be
overidden by supplying an explicit function. Here the builder
function for `:forms` is given explicitly and is passed the
*schatten* and the (lform) `:actual` *verb* 3tuple for the `:forms`
field. It must return `{:ok, forms}` or `{:error, error}`. Here the
three initial `x` forms are "built" into two `a` forms.
Its important to note that once production has started, the single form
and multiple forms can diverge. In this example the `result` has been
otained from evaluating (`Code.eval_quoted/3`) the single form which
is still the "reduced" (`Kernel.SpecialForms.unquote_splicing/1`)
original list of three `forms`.
iex> forms = [
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1),
...> ]
...> form_original = quote do
...> x = x + 1
...> x = x * x
...> x = x - 1
...> end
...> forms_replace = [quote(do: a = a * a), quote(do: a = a + 5)]
...> {:ok, {[
...> forms: ^forms_replace,
...> texts: texts,
...> form: ^form_original,
...> result: 63], _schatten}} = forms
...> |> produce_schatten(
...> binding: [x: 7],
...> build: [forms:
...> fn _schatten, {:actual,:forms,^forms} ->
...> {:ok, forms_replace}
...> end],
...> expect: [forms: forms_replace],
...> express: [:forms, :texts, :form, :result])
...> texts
["a = a * a", "a = a + 5"]
The forms returned by the builder are validated:
iex> form = quote(do: x = x + 1)
...> {:error, error} = form
...> |> produce_schatten(
...> build: [forms:
...> fn _schatten, {:actual,:forms,[^form]} ->
...> {:ok, :forms_from_builder_are_invalid}
...> end],
...> expect: [form: form],
...> express: [:forms, :texts])
...> error |> Exception.message
"forms invalid, got: :forms_from_builder_are_invalid"
The production process is fairly flexible to serve various testing needs.
In this example a field called *custom* is added (i.e. `:actual`),
then built and finally expressed together with other fields.
iex> form = quote(do: x = x + 1)
...> {:ok, {[form: ^form, texts: texts, custom: 84], _schatten}} = form
...> |> produce_schatten(
...> actual: [custom: 42],
...> build: [custom: fn _, {_verb,_field,value} -> {:ok, value * 2} end],
...> expect: [form: form],
...> express: [:form, :texts, :custom])
...> texts
["x = x + 1"]
In the same vein, if an error is expected, it can be caught. Note
the value of the `:error` key below is the `Exception.message/1`
text to compare with. If the compare fails the original `{:error,
error}` is returned. Note the *product* is empty.
iex> form = quote(do: x = x + 1)
...> {:ok, {product, _schatten}} = form
...> |> produce_schatten(
...> binding: [x: 7],
...> expect: [result: 42, error: "compare expect actual failed, got: field: :result, expect: 42; actual: 8"])
...> product
[]
This shows the original error being returned when the error does not match:
iex> form = quote(do: x = x + 1)
...> {:error, error} = form
...> |> produce_schatten(
...> binding: [x: 7],
...> expect: [result: 42, error: "this will not match"])
...> error |> Exception.message
"compare expect actual failed, got: field: :result, expect: 42; actual: 8"
Another couple of `:error` examples but this time the value of the `:error` key
is an arity 1 function that is passed the *error* 3tuple and
should return a *pattern 0 truthy* value (See
`Harnais.Form.Utility.truthy0_result/1`). If the normalised truthy
result is `{:error, error}`, the original `{:error, error}` is returned.
iex> form = quote(do: x = x + 1)
...> {:ok, {product, _schatten}} = form
...> |> produce_schatten(
...> binding: [x: 7],
...> expect: [result: 42, error: fn {:actual,:error,_error} -> true end])
...> product
[]
iex> form = quote(do: x = x + 1)
...> {:error, error} = form
...> |> produce_schatten(
...> binding: [x: 7],
...> expect: [result: 42, error: fn {:actua,:error,_error} -> false end])
...> error |> Exception.message
"compare expect actual failed, got: field: :result, expect: 42; actual: 8"
The initial forms can be a generator function that is passed the *schatten* and can return
`form`, `forms`, `{:ok, form}`, `{:ok, forms}`, `{:ok, product}` or `{:error, error}`.
The `product` must be a `Keyword` and have zero, one of more `:form`
and/or `:forms` keys: the forms are collected together. Other keys are ignored.
iex> generator = fn _schatten -> {:ok, quote(do: x = x + 1)} end
...> {:ok, {[form: quote(do: x = x + 1), text: text], _schatten}} = generator
...> |> produce_schatten(express: [:form, :text])
...> text
"x = x + 1"
iex> generator = fn _schatten -> {:ok, [
...> form: quote(do: x = x + 1)
...> ]}
...> end
...> {:ok, {[form: quote(do: x = x + 1), text: text], _schatten}} = generator
...> |> produce_schatten(express: [:form, :text])
...> text
"x = x + 1"
iex> generator = fn _schatten -> {:ok, [
...> form: quote(do: x = x + 1),
...> will_be_ignored: 42,
...> forms: [quote(do: x = x * x), quote(do: x = x - 1)]
...> ]}
...> end
...> {:ok, {[result: 63, texts: texts], _schatten}} = generator
...> |> produce_schatten(binding: [x: 7], express: [:result, :texts])
...> texts
["x = x + 1", "x = x * x", "x = x - 1"]
Using a `:error` is useful particularly when the forms generator "wraps"
another function that can return an error that needs to be caught and compared.
iex> form_generator = fn _schatten -> {:not, :a, :form} end
...> {:ok, {product, _schatten}} = form_generator
...> |> produce_schatten(expect: [error: "forms generator function failed, got: form invalid, got: {:not, :a, :form}"])
...> product
[]
The same example but here the `:error` is an arity 1 function that
is passed the error 3tuple and the return is normalised to a *pattern
0 truthy result* (See `Harnais.Form.Utility.truthy0_result/1`): A *falsy* result fails the compare.
iex> form_generator = fn _schatten -> {:not, :a, :form} end
...> {:ok, {product, _schatten}} = form_generator
...> |> produce_schatten(expect: [
...> error: fn {:actual,:error,error} -> error |> Exception.message |> String.starts_with?("forms generator function failed") end])
...> product
[]
"""
@spec produce_schatten(any, t) :: {:ok, {forms, t}} | {:error, error}
@spec produce_schatten(any, opts) :: {:ok, {forms, t}} | {:error, error}
def produce_schatten(form, opts \\ [])
def produce_schatten(form, %__MODULE__{} = state) do
with {:ok, %__MODULE__{} = state} <- state |> schatten_update_form(form) do
state
|> produce
|> case do
{:error, %{__struct__: _} = error} ->
with {:ok, {_produce, %__MODULE__{}}} = result <- state |> produce_error(error) do
result
else
# if produce_error fails, return the original error
_ ->
{:error, error}
end
{:ok, {_product, %__MODULE__{}}} = result ->
result
end
else
{:error, %{__exception__: true}} = result -> result
end
end
def produce_schatten(schatten, opts) do
with {:ok, %__MODULE__{} = state} <- opts |> new() do
schatten |> produce_schatten(state)
else
{:error, %{__exception__: true}} = result -> result
end
end
@harnais_form_schatten_defstruct_updaters @harnais_form_schatten_defstruct
|> Enum.map(fn {field, _} ->
update_fun =
"schatten_update_#{field}" |> String.to_atom()
{:pattern,
[
pattern: :struct_update,
name: update_fun,
field: field,
doc: false
]}
end)
(@harnais_form_schatten_defstruct_updaters ++
[
{@plymio_codi_pattern_struct_update,
[
args: [:t, :value],
name: :update_workflow_pipeline,
field: :workflow_pipeline,
doc: false
]},
{@plymio_codi_pattern_proxy_fetch,
[
:workflow_def_produce,
:state_base_package,
:state_defp_update_field_header
]}
])
|> CODI.reify_codi(@codi_opts)
defp update_field(%__MODULE__{} = state, {k, v})
when k in [
@harnais_form_schatten_field_eval_binding
] do
with {:ok, v} <- v |> opts_validate do
{:ok, state |> struct!([{k, v}])}
else
{:error, %{__exception__: true}} = result -> result
end
end
defp update_field(%__MODULE__{} = state, {k, v})
when k == @harnais_form_schatten_field_form do
# force the form/forms bootstrapping
with {:ok, %__MODULE__{} = state} <-
state
|> HASWE.state_edit_workflow_pipeline_add_tail(
{@harnais_form_schatten_workflow_verb_build,
@harnais_form_schatten_field_bootstrap_form, @harnais_form_schatten_value_not_set}
),
true <- true do
{:ok, state |> struct!([{k, v}])}
else
{:error, %{__exception__: true}} = result -> result
end
end
defp update_field(%__MODULE__{} = state, {k, v})
when k in [@harnais_form_schatten_field_transform_opts] do
with {:ok, transform_opts} <- v |> opts_validate do
{:ok, state |> struct!([{k, transform_opts}])}
else
{:error, %{__exception__: true}} = result -> result
end
end
defp update_field(%__MODULE__{} = state, {k, v})
when k in @harnais_form_keys_opts_transform_ast_aliases do
transform_opts =
state
|> Map.get(@harnais_form_schatten_field_transform_opts)
|> case do
@harnais_form_schatten_value_transform_opts_initial -> []
x when is_list(x) -> x
end
|> Keyword.put(k, v)
state |> update_field({@harnais_form_schatten_field_transform_opts, transform_opts})
end
defp update_field(%__MODULE__{} = state, {k, v})
when k in [@harnais_form_schatten_field_workflow_pipeline] do
with {:ok, pipeline} <- v |> HASWU.validate_workflow_pipeline() do
{:ok, state |> struct!([{@harnais_form_schatten_field_workflow_pipeline, pipeline}])}
else
{:error, %{__exception__: true}} = result -> result
end
end
defp update_field(
%__MODULE__{@harnais_form_schatten_field_workflow_pipeline => pipeline} = state,
{k, v}
)
when k in @harnais_form_schatten_workflow_verbs_order do
pipeline_opts = [{@harnais_form_schatten_key_verb, k}]
with {:ok, pipeline_new} <- v |> HASWU.normalise_workflow_pipeline(pipeline_opts) do
pipeline
|> is_value_set
|> case do
true ->
state |> schatten_update_workflow_pipeline(pipeline ++ pipeline_new)
_ ->
state |> schatten_update_workflow_pipeline(pipeline_new)
end
else
{:error, %{__exception__: true}} = result -> result
end
end
defp resolve(state)
defp resolve(%__MODULE__{@harnais_form_schatten_field_workflow_pipeline => pipeline} = state) do
with {:ok, pipeline} <- pipeline |> HASWU.collate_workflow_pipeline(),
true <- true do
pipeline
|> reduce0_enum(
[],
fn {verb, field, args}, pipeline ->
with {:ok, dependencies} <- {verb, field, args} |> HASWD.dependent_workflow_entry(),
{:ok, _pipeline} = result <-
pipeline
|> HASWE.edit_workflow_pipeline([
{@harnais_form_schatten_workflow_pipeline_edit_verb_add_head, dependencies},
{@harnais_form_schatten_workflow_pipeline_edit_verb_add_tail,
{verb, field, args}}
]) do
result
else
{:error, %{__struct__: _}} = result -> result
end
end
)
|> case do
{:error, %{__exception__: true}} = result ->
result
{:ok, pipeline} ->
with {:ok, pipeline} <- pipeline |> HASWU.collate_workflow_pipeline(),
{:ok, %__MODULE__{}} = result <-
state |> schatten_update_workflow_pipeline(pipeline) do
result
else
{:error, %{__exception__: true}} = result -> result
end
end
else
{:error, %{__exception__: true}} = result -> result
end
end
defp build(state)
defp build(%__MODULE__{@harnais_form_schatten_field_workflow_pipeline => pipeline} = state)
when is_list(pipeline) and length(pipeline) == 0 do
{:ok, state}
end
defp build(%__MODULE__{@harnais_form_schatten_field_workflow_pipeline => pipeline} = state) do
with {:ok, pipeline} <- pipeline |> HASWU.collate_workflow_pipeline(),
{:ok, %__MODULE__{} = state} <- state |> schatten_update_workflow_pipeline(pipeline) do
pipeline
|> reduce2_enum(
state,
fn {verb, field, args}, state ->
state |> HASWR.run_workflow_pipeline_tuple({verb, field, args})
end
)
else
{:error, %{__exception__: true}} = result -> result
end
end
defp express(state)
defp express(%__MODULE__{} = state) do
with {:ok, %__MODULE__{} = state} <- state |> resolve,
{:ok, %__MODULE__{} = state} <- state |> build,
{:ok, pipeline} <- state |> Map.fetch(@harnais_form_schatten_field_workflow_pipeline),
{:ok, pipeline} <-
pipeline
|> HASWF.filter_workflow_pipeline_by_verb(@harnais_form_schatten_workflow_verb_produce) do
product =
pipeline
|> Enum.map(&Tuple.delete_at(&1, 0))
{:ok, {product, state}}
else
{:error, %{__exception__: true}} = result -> result
end
end
defp produce_error(schatten, error)
defp produce_error(
%__MODULE__{@harnais_form_schatten_field_workflow_pipeline => pipeline} = state,
error
)
when is_filled_list(pipeline) do
pipeline = state |> Map.get(@harnais_form_schatten_field_workflow_pipeline)
# check if there is there are error tuples
with {:ok, error_pipeline} <-
pipeline
|> HASWF.filter_workflow_pipeline_by_field(@harnais_form_schatten_field_error) do
error_pipeline
|> length
|> case do
0 ->
{:error, error}
_ ->
with {:ok, %__MODULE__{} = state} <- state |> update_workflow_pipeline(error_pipeline),
{:ok, %__MODULE__{} = state} <-
state
|> HASWE.state_edit_workflow_pipeline_add_tail(
{@harnais_form_schatten_workflow_verb_actual,
@harnais_form_schatten_field_error, error}
),
{:ok, {_product, %__MODULE__{}}} = result <- state |> produce do
result
else
{:error, %{__exception__: true}} = result -> result
end
end
else
{:error, %{__struct__: _}} = result -> result
end
end
defp produce_error(%__MODULE__{} = state, _error) do
new_error_result(m: "workflow pipeline empty", v: state)
end
end