plymio_codi v0.3.1 Plymio.Codi.Pattern.Struct View Source

The struct patterns create a range of transform functions for a module’s struct.

See Plymio.Codi for an overview and documentation terms.

Set and Unset Fields

These patterns use the unset value (see Plymio.Fontais.Guard.the_unset_value/0) to decide whether a field has a useful value. Fields can be unset by default by using the unset value in the Kernel.defstruct/1 e.g.

 defstruct [x: Plymio.Fontais.Guard.the_unset_value()]

For example, a function built using the struct_fetch pattern will return {error, error} if the target field’s value is unset. Similary, for struct_get, {:ok, default} will be returned if the field’s value is unset.

Errors

The code generated by most struct patterns checks the first argument is an instance of the target module’s struct and returns {:error, error} if not.

Test Environment

The doctests use a helper (codi_helper_struct_compile_module/1) to compile the generated function(s) in a dynamically created module, returning {:ok, {forms, test_mod}}.

The forms are the generated code and another helper (Harnais.Helper.harnais_helper_format_forms!/2) is used to “textify” the code using the Elixir code formatter.

The test_mod is the dynamically created module and is used to call the generated function(s).

The default Kernel.defstruct/1 for the doctests is shown here. Note the :z field is unset.

 defstruct [x: 42, y: nil, z: Plymio.Fontais.Guard.the_unset_value()]

The first example for each pattern just shows the generated code with subsequent examples performing the actual test(s).

Common Codi Pattern Opts (cpo) Keys

As well as the top level common options, these keys are valid in any struct pattern’s cpo:

KeyAliases
:fun_name:name, :function_name
:fun_field:field, ::function_field, :key, :fun_key, :function_key
:fun_args:args, :function_args
:fun_arity:arity, :function_arity
:fun_doc:doc, :function_doc
:typesepc_spec_args:spec_args
:typespec_spec_result:spec_result, :result, :fun_result, :function_result
:since

All struct patterns support the generation of @doc, @spec and @since module attribute forms.

All struct patterns generate a @doc by default. (It can be disabled in the usual way by specifying doc: false in the cpo.)

In the doctests below the @doc forms are mostly disabled (i.e. doc: nil) for clarity.

Pattern: struct_get

This pattern is a convenience to generate both struct_get1 and struct_get2 patterns.

The cpo must be configured for a struct_get2 and will be “reduced” to suite a struct_get1.

Pattern: struct_get1

The struct_get1 pattern creates a function to get the value of a struct’s field and if the value is unset return a fixed default.

Examples

This example generate a get/1 function for the :z field. Note the :field in the cpo is a Keyword with the fixed default for the :z field ("z get default")

iex> {:ok, {forms, _test_mod}} = [
...>    struct_get1: [args: :t, name: :get_z, field: [z: "z get default"], doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(get_z(t))",
 "",
 "def(get_z(%__MODULE__{z: field_value})) do",
 "  field_value",
 "  |> case do",
 "    x when Plymio.Fontais.Guard.is_value_set(x) ->",
 "      {:ok, x}",
 "",
 "    _ ->",
 "      {:ok, \"z get default\"}",
 "  end",
 "end",
 "",
 "def(get_z(state)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

iez> {:ok, {_forms, test_mod}} = [
...>    struct_get1: [args: :t, name: :get_z, field: [z: "z get default"], doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # z is unset by default so the get/1 default will be returned.
...> struct(test_mod) |> test_mod.get_z
{:ok, "z get default"}

Pattern: struct_get2

The struct_get2 pattern creates a function to get the value of a struct’s field and, if the value is unset, return the second argument.

Examples

This example generate a get/2 function for the :z field.

iex> {:ok, {forms, _test_mod}} = [
...>    struct_get2: [args: [:t, :the_default_for_z], name: :get_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(get_z(t, the_default_for_z))",
 "",
 "def(get_z(%__MODULE__{z: field_value}, default)) do",
 "  field_value",
 "  |> case do",
 "    x when Plymio.Fontais.Guard.is_value_set(x) ->",
 "      {:ok, x}",
 "",
 "    _ ->",
 "      {:ok, default}",
 "  end",
 "end",
 "",
 "def(get_z(state, _default)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

iez> {:ok, {_forms, test_mod}} = [
...>    struct_get2: [args: [:t, :the_default_for_z], name: :get_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # z is unset by default so the get/2 2nd argument will be returned.
...> struct(test_mod) |> test_mod.get_z("an explicit default")
{:ok, "an explicit default"}

Pattern: struct_fetch

The struct_fetch pattern creates a function to fetch the value of a struct’s field.

Examples

This example generate a fetch/1 function for the :x field.

iex> {:ok, {forms, _test_mod}} = [
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(fetch_x(t))",
 "",
 "def(fetch_x(%__MODULE__{x: field_value} = state)) do",
 "  field_value",
 "  |> case do",
 "    x when Plymio.Fontais.Guard.is_value_set(x) ->",
 "      {:ok, x}",
 "",
 "    _ ->",
 "      Plymio.Codi.Error.new_error_result(m: \"struct field \#{:x} unset\", v: state)",
 "  end",
 "end",
 "",
 "def(fetch_x(state)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

iex> {:ok, {_forms, test_mod}} = [
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> {:ok, 42} = struct(test_mod) |> test_mod.fetch_x
...> # setting x to the unset value causes the fetch to fail
...> {:error, error} = struct(test_mod, x: Plymio.Fontais.Guard.the_unset_value) |> test_mod.fetch_x
...> true = error |> Exception.message |> String.starts_with?("struct field x unset")
...> # the argument must be an instance of the module's struct
...> {:error, error} = :not_a_struct |> test_mod.fetch_x
...> error |> Exception.message
"struct invalid, got: :not_a_struct"

Pattern: struct_put

The struct_put pattern creates a function to put a value for a struct’s field.

Examples

This example generates a put/2 function for the :x field.

iex> {:ok, {forms, _test_mod}} = [
...>    struct_put: [args: [:t, :value], name: :put_x, field: :x, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(put_x(t, value))",
 "",
 "def(put_x(%__MODULE__{x: _} = state, value)) do",
 "  {:ok, state |> struct!(x: value)}",
 "end",
 "",
 "def(put_x(state, _value)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

iex> {:ok, {_forms, test_mod}} = [
...>    struct_put: [args: [:t, :value], name: :put_x, field: :x, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # set the :x field's value to 123
...> {:ok, %test_mod{} = t1} = struct(test_mod) |> test_mod.put_x(123)
...> # use `fetch_x/1` to check
...> {:ok, 123} = t1 |> test_mod.fetch_x
...> # the argument must be an instance of the module's struct
...> {:error, error} = :not_a_struct |> test_mod.put_x(123)
...> error |> Exception.message
"struct invalid, got: :not_a_struct"

Pattern: struct_maybe_put

The struct_maybe_put pattern create a function to put a value for a struct’s field only if the field’s current value is unset.

Examples

This code shows a maybe_put/2 function for the :z field:

iex> {:ok, {forms, _test_mod}} = [
...>    struct_maybe_put: [args: [:t, :value], name: :maybe_put_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(maybe_put_z(t, value))",
 "",
 "def(",
 "  maybe_put_z(%__MODULE__{z: field_value} = state, _value)",
 "  when Plymio.Fontais.Guard.is_value_set(field_value)",
 ") do",
 "  {:ok, state}",
 "end",
 "",
 "def(",
 "  maybe_put_z(%__MODULE__{z: field_value} = state, value)",
 "  when Plymio.Fontais.Guard.is_value_unset(field_value)",
 ") do",
 "  value",
 "  |> Plymio.Fontais.Guard.is_value_unset()",
 "  |> case do",
 "    true ->",
 "      {:ok, state}",
 "",
 "    _ ->",
 "      {:ok, state |> struct!(z: value)}",
 "  end",
 "end",
 "",
 "def(maybe_put_z(state, _value)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

Here maybe_put/2 and fetch/1 functions are generated for two fields: the :x field has 42 for its default, whereas the :z field is unset.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_maybe_put: [args: [:t, :value], name: :maybe_put_x, field: :x, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...>    struct_maybe_put: [args: [:t, :value], name: :maybe_put_z, field: :z, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # by default the `:z` field is unset so `fetch_z/1` will fail
...> t1 = struct(test_mod)
...> {:error, _error} = t1 |> test_mod.fetch_z
...> # maybe_put/2 will update the field since it is unset
...> {:ok, %test_mod{} = t1} = t1 |> test_mod.maybe_put_z(123)
...> # use `fetch_z/1` to check
...> {:ok, 123} = t1 |> test_mod.fetch_z
...> # field `:x` has a default of `42` so maybe_put/2 will not update the field
...> {:ok, %test_mod{} = t1} = t1 |> test_mod.maybe_put_x("will be ignored")
...> # the `:x` field will still be `42`
...> t1 |> test_mod.fetch_x
{:ok, 42}

Pattern: struct_has?

The struct_has? pattern creates a function that returns true if the field’s value is set, otherwise false.

Note: if the first argument is not a valid struct, false is returned..

Examples

This code shows has?/1 function for the :z field:

iex> {:ok, {forms, _test_mod}} = [
...>    struct_has?: [args: :t, name: :has_z?, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(has_z?(t))",
 "",
 "def(has_z?(%__MODULE__{z: field_value}) when Plymio.Fontais.Guard.is_value_set(field_value)) do",
 "  true",
 "end",
 "",
 "def(has_z?(_state)) do",
 "  false",
 "end"]

Here has?/1 functions are generated for two fields: the :x field has 42 for its default, whereas the :z field is unset.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_has?: [args: :t, name: :has_x?, field: :x, doc: nil],
...>    struct_has?: [args: :t, name: :has_z?, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> t1 = struct(test_mod)
...> false = t1 |> test_mod.has_z?
...> t1 |> test_mod.has_x?
true

Pattern: struct_update

The struct_update pattern creates a function to call the modules’s update/2 function.

The module’s update/2 function is a standard Plymio module state function that works like a validated put. Apart from showing the generated code, it is not documented or tested further here.

Examples

This example generates an update/2 function for the :x field.

iex> {:ok, {forms, _codi}} = [
...>    struct_update: [args: [:t, :value], name: :update_x, field: :x, doc: nil],
...> ] |> CODI.produce_codi
...> forms |> harnais_helper_format_forms!
["def(update_x(t, value))",
 "",
 "def(update_x(%__MODULE__{x: _} = state, value)) do",
 "  state |> update(x: value)",
 "end",
 "",
 "def(update_x(state, _value)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

Pattern: struct_set

The struct_set pattern is a simple but versatile pattern for creating a function that sets one or more fields in the struct to specific values, defaulting to the unset value.

Examples

This example generates an set/2 function to set the :x field to value 123. Note the :field in the cpo is a Keyword.

iex> {:ok, {forms, _codi}} = [
...>    struct_set: [args: :t, name: :set_x, field: [x: 123], doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(set_x(t))",
 "",
 "def(set_x(%__MODULE__{x: _} = state)) do",
 "  {:ok, state |> struct!(x: 123)}",
 "end",
 "",
 "def(set_x(state)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

This example create a set/1 function that sets the :x and :z fields.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_set: [args: :t, name: :set_xz, doc: nil,
...>    field: [x: 123, z: "z is no longer unset"]],
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_y, field: :y, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # set the :x and :z fields
...> {:ok, %test_mod{} = t1} = struct(test_mod) |> test_mod.set_xz
...> # use fetch to check
...> {:ok, 123} = t1 |> test_mod.fetch_x
...> {:ok, nil} = t1 |> test_mod.fetch_y
...> t1 |> test_mod.fetch_z
{:ok, "z is no longer unset"}

This example create a set/1 function that sets the :x and :z fields to specific values, but unsets the :y field. Note the :field in the cpo is not a Keyword: The value for :y is not given and defaults to be the unset value.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_set: [args: :t, name: :set_xyz, doc: nil,
...>    field: [{:x, 123}, :y, {:z, "z is no longer unset"}]],
...>    struct_fetch: [args: :t, name: :fetch_x, field: :x, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_y, field: :y, doc: nil],
...>    struct_fetch: [args: :t, name: :fetch_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # set all 3 fields
...> {:ok, %test_mod{} = t1} = struct(test_mod) |> test_mod.set_xyz
...> {:ok, 123} = t1 |> test_mod.fetch_x
...> {:ok, "z is no longer unset"} = t1 |> test_mod.fetch_z
...> # :y is now unset
...> {:error, error} = t1 |> test_mod.fetch_y
...> error |> Exception.message |> String.starts_with?("struct field y unset")
true

Pattern: struct_export

The struct_export pattern creates a function that exports one or more fields in the struct to an opts (Keyword).

The export (opts) is sparse: only keys that are set are included.

Default values can be provided; if the value of the key in the struct is unset, the default is used.

Examples

This example generates an export/1 function for all three fields in the test struct. Note, since no default export values were given in the :field in the cpo, the defaults are the unset value and the field will only appear in the export if the struct value is set.

iex> {:ok, {forms, _codi}} = [
...>    struct_export: [args: :t, name: :export_all, field: [:x, :y, :z], doc: nil],
...> ] |> codi_helper_struct_compile_module
...> forms |> harnais_helper_format_forms!
["def(export_all(t))", "",
 "def(export_all(%__MODULE__{x: field_value, y: field_value1, z: field_value2})) do",
 "  tuples =",
 "    [",
 "      x: :plymio_fontais_t3h1e4_u9n8s7e2t7_v1a8l3u8e,",
 "      y: :plymio_fontais_t3h1e4_u9n8s7e2t7_v1a8l3u8e,",
 "      z: :plymio_fontais_t3h1e4_u9n8s7e2t7_v1a8l3u8e",
 "    ] ++ [x: field_value, y: field_value1, z: field_value2]",
 "",
 "  export =",
 "    tuples",
 "    |> Keyword.keys()",
 "    |> Stream.uniq()",
 "    |> Stream.map(fn k ->",
 "      tuples",
 "      |> Keyword.get_values(k)",
 "      |> Enum.filter(fn v -> v |> Plymio.Fontais.Guard.is_value_set() end)",
 "      |> case do",
 "        [] ->",
 "          {k, @plymio_fontais_the_unset_value}",
 "",
 "        values ->",
 "          {k, values |> List.last()}",
 "      end",
 "    end)",
 "    |> Stream.filter(fn {_k, v} -> v |> Plymio.Fontais.Guard.is_value_set() end)",
 "    |> Keyword.new()",
 "",
 "  {:ok, export}",
 "end",
 "",
 "def(export_all(state)) do",
 "  Plymio.Codi.Error.new_error_result(m: \"struct invalid\", v: state)",
 "end"]

This example creates an export/1 function that exports the :x and :z fields. The :z field is unset by default so will not appear in the export unless set explicitly.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_export: [args: :t, name: :export_xz, doc: nil,
...>    field: [:x, :z]],
...>    struct_put: [args: [:t, :value], name: :put_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # the :z field is by default unset and will not be in the export
...> {:ok, [x: 42]} = struct(test_mod) |> test_mod.export_xz
...> # set z and export
...> {:ok, %test_mod{} = t1} = struct(test_mod) |> test_mod.put_z("z is now set")
...> t1 |> test_mod.export_xz
{:ok, [x: 42, z: "z is now set"]}

Another example but providing default values for each key in the export by supplying a Keyword for :field in the cpo:.

iex> {:ok, {_forms, test_mod}} = [
...>    struct_export: [args: :t, name: :export_xz, doc: nil,
...>    field: [x: :x_default, z: :z_default]],
...>    struct_put: [args: [:t, :value], name: :put_z, field: :z, doc: nil],
...> ] |> codi_helper_struct_compile_module
...> # the :z field has a default export value
...> struct(test_mod) |> test_mod.export_xz
{:ok, [x: 42, z: :z_default]}