diff --git a/lib/parameter/field.ex b/lib/parameter/field.ex index 00b5976..bae49c6 100644 --- a/lib/parameter/field.ex +++ b/lib/parameter/field.ex @@ -27,11 +27,11 @@ defmodule Parameter.Field do * `:virtual` - If `true` the field will be ignored on `Parameter.load/3` and `Parameter.dump/3` functions. - * `:load_func` - Function to specify how to load the field. The function must have two arguments where the first one is the field value and the second one - will be the data to be loaded. + * `:on_load` - Function to specify how to load the field. The function must have two arguments where the first one is the field value and the second one + will be the data to be loaded. Should return `{:ok, value}` or `{:error, reason}` tuple. - * `:dump_func` - Function to specify how to dump the field. The function must have two arguments where the first one is the field value and the second one - will be the data to be dumped. + * `:on_dump` - Function to specify how to dump the field. The function must have two arguments where the first one is the field value and the second one + will be the data to be dumped. Should return `{:ok, value}` or `{:error, reason}` tuple. > NOTE: Validation only occurs on `Parameter.load/3`. > By desgin, data passed into `Parameter.dump/3` are considered valid. @@ -49,8 +49,8 @@ defmodule Parameter.Field do :default, :load_default, :dump_default, - :load_func, - :dump_func, + :on_load, + :on_dump, type: :string, required: false, validator: nil, @@ -63,8 +63,8 @@ defmodule Parameter.Field do default: any(), load_default: any(), dump_default: any(), - load_func: fun() | nil, - dump_func: fun() | nil, + on_load: fun() | nil, + on_dump: fun() | nil, type: Types.t(), required: boolean(), validator: fun() | nil, @@ -104,8 +104,8 @@ defmodule Parameter.Field do default = Keyword.get(opts, :default) load_default = Keyword.get(opts, :load_default) dump_default = Keyword.get(opts, :dump_default) - load_func = Keyword.get(opts, :load_func) - dump_func = Keyword.get(opts, :dump_func) + on_load = Keyword.get(opts, :on_load) + on_dump = Keyword.get(opts, :on_dump) required = Keyword.get(opts, :required, false) validator = Keyword.get(opts, :validator) virtual = Keyword.get(opts, :virtual, false) @@ -115,8 +115,8 @@ defmodule Parameter.Field do :ok <- Types.validate(:string, key), :ok <- Types.validate(:boolean, required), :ok <- Types.validate(:boolean, virtual), - :ok <- load_func_valid?(load_func), - :ok <- dump_func_valid?(dump_func), + :ok <- on_load_valid?(on_load), + :ok <- on_dump_valid?(on_dump), :ok <- validator_valid?(validator) do struct!(__MODULE__, opts) end @@ -155,12 +155,12 @@ defmodule Parameter.Field do end end - defp load_func_valid?(load_func) do - function_valid?(load_func, 2, "load_func must be a function") + defp on_load_valid?(on_load) do + function_valid?(on_load, 2, "on_load must be a function") end - defp dump_func_valid?(dump_func) do - function_valid?(dump_func, 2, "dump_func must be a function") + defp on_dump_valid?(on_dump) do + function_valid?(on_dump, 2, "on_dump must be a function") end defp validator_valid?(validator) do diff --git a/lib/parameter/schema.ex b/lib/parameter/schema.ex index 64da99e..e93e7dc 100644 --- a/lib/parameter/schema.ex +++ b/lib/parameter/schema.ex @@ -89,14 +89,12 @@ defmodule Parameter.Schema do schema = %{ first_name: [key: "firstName", type: :string, required: true], - address: [required: true, type: {:has_one, %{street: [type: :string, required: true]}}], + address: [type: {:has_one, %{street: [type: :string, required: true]}}], phones: [type: {:has_many, %{country: [type: :string, required: true]}}] - } + } |> Parameter.Schema.compile!() - compiled_schema = Parameter.Schema.compile!(schema) - - Parameter.load(compiled_schema, %{"firstName" => "John"}) - ... + Parameter.load(schema, %{"firstName" => "John"}) + {:ok, %{first_name: "John"}} The same API can also be evaluated on compile time by using module attributes: @@ -118,6 +116,7 @@ defmodule Parameter.Schema do This makes it easy to dynamically create schemas or just avoid using any macros. ## Required fields + By default, `Parameter.Schema` considers all fields to be optional when validating the schema. This behaviour can be changed by passing the module attribute `@fields_required true` on the module where the schema is declared. @@ -136,6 +135,49 @@ defmodule Parameter.Schema do Parameter.load(MyApp.UserSchema, %{}) {:error, %{age: "is required", name: "is required"}} + + + ## Custom field loading and dumping + + The `load` and `dump` behavior can be customized per field by implementing `on_load` or `on_dump` functions in the field definition. + This can be useful if the field needs to be fetched or even validate in a different way than the defaults implemented by `Parameter`. + Both functions should return `{:ok, value}` or `{:error, reason}` tuple. + + For example, imagine that there is a parameter called `full_name` in your schema that you want to customize on how it will be parsed: + + defmodule MyApp.UserSchema do + use Parameter.Schema + + param do + field :first_name, :string + field :last_name, :string + field :full_name, :string, on_load: &__MODULE__.load_full_name/2 + end + + def load_full_name(value, params) do + # if `full_name` is not `nil` it just return the `full_name` + if value do + {:ok, value} + else + # Otherwise it will join the `first_name` and `last_name` params + {:ok, params["first_name"] <> " " <> params["last_name"]} + end + end + end + + Now when loading, the full_name field will be handled by the `load_full_name/2` function: + + Parameter.load(MyApp.UserSchema, %{first_name: "John", last_name: "Doe", full_name: nil}) + {:ok, %{first_name: "John", full_name: "John Doe", last_name: "Doe"}} + + The same behavior is possible when dumping the schema parameters by using `on_dump/2` function: + + schema = %{ + level: [type: :integer, on_dump: fn value, _input -> {:ok, value || 0} end] + } |> Parameter.Schema.compile!() + + Parameter.dump(schema, %{level: nil}) + {:ok, %{"level" => 0}} """ alias Parameter.Field diff --git a/lib/parameter/schema/compiler.ex b/lib/parameter/schema/compiler.ex index 22118d0..c801bb4 100644 --- a/lib/parameter/schema/compiler.ex +++ b/lib/parameter/schema/compiler.ex @@ -20,12 +20,12 @@ defmodule Parameter.Schema.Compiler do raise ArgumentError, "validator cannot be used on nested fields" end - if :load_func in keys do - raise ArgumentError, "load_func cannot be used on nested fields" + if :on_load in keys do + raise ArgumentError, "on_load cannot be used on nested fields" end - if :dump_func in keys do - raise ArgumentError, "dump_func cannot be used on nested fields" + if :on_dump in keys do + raise ArgumentError, "on_dump cannot be used on nested fields" end opts diff --git a/lib/parameter/schema_fields.ex b/lib/parameter/schema_fields.ex index fc854f9..d1d4f30 100644 --- a/lib/parameter/schema_fields.ex +++ b/lib/parameter/schema_fields.ex @@ -106,13 +106,13 @@ defmodule Parameter.SchemaFields do end def field_handler( - %Meta{parent_input: parent_input, operation: operation} = meta, - %Field{load_func: load_func} = field, + %Meta{parent_input: parent_input, operation: :load} = meta, + %Field{on_load: on_load} = field, value, opts ) - when not is_nil(load_func) and operation in [:load] do - case load_func.(value, parent_input) do + when not is_nil(on_load) do + case on_load.(value, parent_input) do {:ok, value} -> operation_handler(meta, field, value, opts) @@ -122,13 +122,13 @@ defmodule Parameter.SchemaFields do end def field_handler( - %Meta{parent_input: parent_input, operation: operation} = meta, - %Field{dump_func: dump_func} = field, + %Meta{parent_input: parent_input, operation: :dump} = meta, + %Field{on_dump: on_dump} = field, value, opts ) - when not is_nil(dump_func) and operation in [:dump] do - case dump_func.(value, parent_input) do + when not is_nil(on_dump) do + case on_dump.(value, parent_input) do {:ok, value} -> operation_handler(meta, field, value, opts) error -> error end @@ -200,6 +200,10 @@ defmodule Parameter.SchemaFields do error end + defp operation_handler(_meta, _field, nil, _opts) do + {:ok, nil} + end + defp operation_handler(meta, %Field{type: type}, value, _opts) do case meta.operation do :dump -> Types.dump(type, value) @@ -226,7 +230,10 @@ defmodule Parameter.SchemaFields do check_required(field, :ignore, meta.operation) {:ok, nil} -> - check_required(field, nil, meta.operation) + case check_required(field, nil, meta.operation) do + {:ok, value} -> field_handler(meta, field, value, opts) + other -> other + end {:ok, value} -> field_handler(meta, field, value, opts) diff --git a/lib/parameter/types.ex b/lib/parameter/types.ex index 0615a77..2609079 100644 --- a/lib/parameter/types.ex +++ b/lib/parameter/types.ex @@ -89,7 +89,7 @@ defmodule Parameter.Types do end end - @spec validate(atom(), any()) :: :ok | {:error, any()} + @spec validate(atom() | composite_types(), any()) :: :ok | {:error, any()} def validate(type, values) def validate({:has_one, inner_type}, values) when is_map(values) do diff --git a/test/parameter/schema/compiler_test.exs b/test/parameter/schema/compiler_test.exs index 6dfd4ea..16bc9ae 100644 --- a/test/parameter/schema/compiler_test.exs +++ b/test/parameter/schema/compiler_test.exs @@ -16,12 +16,12 @@ defmodule Parameter.Schema.CompilerTest do Compiler.fetch_nested_opts!(dump_default: nil) end - assert_raise ArgumentError, "load_func cannot be used on nested fields", fn -> - Compiler.fetch_nested_opts!(load_func: nil) + assert_raise ArgumentError, "on_load cannot be used on nested fields", fn -> + Compiler.fetch_nested_opts!(on_load: nil) end - assert_raise ArgumentError, "dump_func cannot be used on nested fields", fn -> - Compiler.fetch_nested_opts!(dump_func: nil) + assert_raise ArgumentError, "on_dump cannot be used on nested fields", fn -> + Compiler.fetch_nested_opts!(on_dump: nil) end assert_raise ArgumentError, "validator cannot be used on nested fields", fn -> diff --git a/test/parameter_test.exs b/test/parameter_test.exs index b6bd968..fc4868a 100644 --- a/test/parameter_test.exs +++ b/test/parameter_test.exs @@ -64,7 +64,7 @@ defmodule ParameterTest do param do field :first_name, :string, key: "firstName", required: true field :last_name, :string, key: "lastName", required: true, default: "" - field :age, :integer, load_func: &__MODULE__.load_age/2 + field :age, :integer, on_load: &__MODULE__.load_age/2 field :metadata, :map, dump_default: %{"key" => "value"} field :hex_amount, CustomTypeHexToDecimal, key: "hexAmount", default: "0" field :paid_amount, :decimal, key: "paidAmount", default: Decimal.new("1") @@ -79,16 +79,16 @@ defmodule ParameterTest do field :type, :string field :age, :integer, - load_func: &ParameterTest.UserTestSchema.load_info_age/2, - dump_func: &ParameterTest.UserTestSchema.dump_age/2 + on_load: &ParameterTest.UserTestSchema.load_info_age/2, + on_dump: &ParameterTest.UserTestSchema.dump_age/2 end has_many :info, Info do field :id, :string field :age, :string, - load_func: &ParameterTest.UserTestSchema.load_info_age/2, - dump_func: &ParameterTest.UserTestSchema.dump_info_age/2 + on_load: &ParameterTest.UserTestSchema.load_info_age/2, + on_dump: &ParameterTest.UserTestSchema.dump_info_age/2 end end @@ -117,7 +117,7 @@ defmodule ParameterTest do end def dump_age(value, input) do - if age = input[:id_info][:age] do + if age = input.id_info.age do {:ok, age} else {:ok, value}