diff --git a/lib/parameter/engine.ex b/lib/parameter/engine.ex new file mode 100644 index 0000000..14bbd6f --- /dev/null +++ b/lib/parameter/engine.ex @@ -0,0 +1,396 @@ +defmodule Parameter.Engine do + alias Parameter.Field + alias Parameter.Schema + alias Parameter.Types + + @type t :: %__MODULE__{ + schema: module() | nil, + fields: list(Field.t()), + valid?: boolean(), + data: map(), + changes: map(), + errors: map(), + cast_fields: list(atom()), + operation: :load | :dump | :validate + } + + defstruct schema: nil, + fields: [], + valid?: true, + data: nil, + changes: %{}, + errors: %{}, + cast_fields: [], + operation: :load + + def asdf() do + %{ + first_name: [type: :string, key: "firstName", required: true], + info: [type: {:map, %{number: [type: {:array, %{number: [type: :integer]}}]}}], + address: [type: {:array, %{number: [type: :integer]}}] + } + |> Schema.compile!() + |> load( + %{ + "firstName" => "Ola", + "info" => %{ + "number" => [%{"number" => "2"}, %{"number" => "asdajsh"}, %{"number" => "what"}] + } + }, + struct: true + ) + |> apply_operation() + end + + @spec load(module | list(Field.t()), map(), Keyword.t()) :: t() + def load(schema_or_fields, params, opts \\ []) do + fields = Schema.fields(schema_or_fields) + + %__MODULE__{ + schema: Schema.module(schema_or_fields), + fields: fields, + data: params, + cast_fields: infer_cast_fields(fields), + operation: :load + } + |> cast_and_load_params(opts) + end + + def apply_operation(%__MODULE__{} = engine) do + engine + |> case do + %__MODULE__{valid?: true} -> + {:ok, fetch_engine_changes(engine)} + + %__MODULE__{valid?: false} -> + {:error, fetch_engine_errors(engine)} + end + end + + defp fetch_engine_changes(%__MODULE__{changes: changes}) when is_map(changes) do + Enum.reduce(changes, changes, fn + {field_key, %__MODULE__{} = engine}, acc -> + Map.put(acc, field_key, fetch_engine_changes(engine)) + + {field_key, values}, acc when is_list(values) -> + Enum.map(values, fn + %__MODULE__{} = engine -> + fetch_engine_changes(engine) + + value -> + value + end) + |> then(fn list -> + Map.put(acc, field_key, list) + end) + + _, acc -> + acc + end) + end + + defp fetch_engine_errors(%__MODULE__{errors: errors, changes: changes}) do + Enum.reduce(changes, errors, fn + {field_key, values}, acc when is_list(values) -> + values + |> Enum.with_index() + |> Enum.filter(fn {engine, _index} -> !engine.valid? end) + |> Enum.map(fn {engine, index} -> %{index => engine.errors} end) + |> then(fn list -> + Map.put(acc, field_key, list) + end) + + {field_key, %__MODULE__{valid?: false} = engine}, acc -> + Map.merge(acc, %{field_key => fetch_engine_errors(engine)}) + + _, acc -> + acc + end) + end + + def add_change(%__MODULE__{changes: changes} = engine, field, change) do + %__MODULE__{engine | changes: Map.put(changes, field, change)} + end + + def get_change(%__MODULE__{changes: changes}, field) do + Map.get(changes, field) + end + + def add_error(%__MODULE__{errors: errors} = engine, field, error) do + %__MODULE__{engine | valid?: false, errors: Map.put(errors, field, error)} + end + + def operation(%__MODULE__{} = engine, operation) when operation in [:load, :dump, :validate] do + %__MODULE__{engine | operation: operation} + end + + defp cast_and_load_params( + %__MODULE__{ + fields: fields, + cast_fields: cast_fields + } = engine, + opts + ) do + fields_to_exclude = + fields + |> Enum.map(& &1.name) + |> Enum.reject(fn field -> field in cast_fields end) + + opts_with_fields_to_exclude = Keyword.merge(opts, exclude: fields_to_exclude) + cast_params(engine, opts_with_fields_to_exclude) + end + + defp cast_params(%__MODULE__{fields: fields, cast_fields: cast_fields} = engine, opts) do + Enum.reduce(cast_fields, engine, fn field_name, engine -> + field = Enum.find(fields, &(&1.name == field_name)) + fetch_and_verify_input(engine, field, opts) + end) + end + + defp infer_cast_fields(fields) do + Enum.map(fields, & &1.name) + end + + defp fetch_and_verify_input(engine, field, opts) do + case fetch_input(engine, field) do + :error -> + check_required(engine, field, :ignore) + + {:ok, nil} -> + check_nil(engine, field, opts) + + {:ok, ""} -> + check_empty(engine, field, opts) + + {:ok, value} -> + handle_field(engine, field, value, opts) + + {:error, reason} -> + add_error(engine, field.name, reason) + end + end + + defp fetch_input(%__MODULE__{data: data, operation: :load}, field) do + fetched_input = Map.fetch(data, field.key) + + if to_string(field.name) == field.key do + verify_double_key(fetched_input, field, data) + else + fetched_input + end + end + + defp fetch_input(%__MODULE__{data: data}, field) do + Map.fetch(data, field.name) + end + + defp verify_double_key(:error, field, input) do + Map.fetch(input, field.name) + end + + defp verify_double_key(fetched_input, field, input) do + case Map.fetch(input, field.name) do + {:ok, _value} -> + {:error, "field is present as atom and string keys"} + + _ -> + fetched_input + end + end + + defp check_required( + %__MODULE__{operation: :load} = engine, + %Field{name: name, required: true, load_default: nil}, + value + ) + when value in [:ignore, nil] do + add_error(engine, name, "is required") + end + + defp check_required( + %__MODULE__{operation: :validate} = engine, + %Field{name: name, required: true, dump_default: nil}, + value + ) + when value in [:ignore, nil] do + add_error(engine, name, "is required") + end + + defp check_required( + %__MODULE__{operation: :load} = engine, + %Field{name: name, load_default: default}, + :ignore + ) + when not is_nil(default) do + add_change(engine, name, default) + end + + defp check_required( + %__MODULE__{operation: :dump} = engine, + %Field{name: name, dump_default: default}, + :ignore + ) + when not is_nil(default) do + add_change(engine, name, default) + end + + defp check_required(%__MODULE__{} = engine, _field, :ignore) do + engine + end + + defp check_required(%__MODULE__{} = engine, %Field{name: name}, value) do + add_change(engine, name, value) + end + + defp check_nil(engine, field, opts) do + if opts[:ignore_nil] do + check_required(engine, field, :ignore) + else + check_required(engine, field, nil) + end + end + + defp check_empty(engine, field, opts) do + if opts[:ignore_empty] do + check_required(engine, field, :ignore) + else + check_required(engine, field, "") + end + end + + defp handle_field(engine, %Field{virtual: true}, _value, _opts) do + engine + end + + defp handle_field(engine, %Field{type: {:array, schema}} = field, values, opts) + when is_list(values) do + values + |> Enum.reverse() + |> Enum.reduce(engine, fn value, engine -> + case handle_method(engine, schema, value, opts) do + %__MODULE__{valid?: false} = inner_engine -> + field_changes = get_change(engine, field.name) || [] + engine = add_change(engine, field.name, [inner_engine | field_changes]) + %__MODULE__{engine | valid?: false} + + %__MODULE__{valid?: true} = inner_engine -> + field_changes = get_change(engine, field.name) || [] + add_change(engine, field.name, [inner_engine | field_changes]) + end + end) + end + + defp handle_field(engine, %Field{name: name, type: {:array, _schema}}, _values, _opts) do + add_error(engine, name, "invalid array type") + end + + defp handle_field(engine, %Field{type: {:map, schema}} = field, value, opts) + when is_map(value) do + handle_field(engine, schema, value, opts) |> IO.inspect() + # value + # |> Enum.reduce(engine, fn {key, value}, engine -> + # case handle_method(engine, schema, value, opts) do + # %__MODULE__{valid?: false} = inner_engine -> + # IO.inspect("whats happening") + # IO.inspect(key) + # IO.inspect(inner_engine) + + # %__MODULE__{valid?: true} = inner_engine -> + # IO.inspect("Im valid ") + # IO.inspect(key) + # IO.inspect(key) + # IO.inspect(inner_engine) + # # add_change(engine, field.name, inner_engine) + # end + # end) + + # case handle_method(engine, schema, value, opts) do + # %__MODULE__{valid?: false} = inner_engine -> + # %__MODULE__{ + # engine + # | changes: Map.put(engine.changes, field.name, inner_engine), + # valid?: false + # } + + # %__MODULE__{valid?: true} = inner_engine -> + # add_change(engine, field.name, inner_engine) + # end + end + + defp handle_field(engine, %Field{name: name, type: {:map, _schema}}, _values, _opts) do + add_error(engine, name, "invalid map type") + end + + defp handle_field( + %__MODULE__{operation: :load} = engine, + %Field{type: type} = field, + value, + _opts + ) do + case Types.load(type, value) do + {:error, error} -> + add_error(engine, field.name, error) + + {:ok, loaded_value} -> + add_change(engine, field.name, loaded_value) + end + end + + defp handle_method(%__MODULE__{operation: :load}, {:map, schema}, params, opts) do + load(schema, params, opts) + |> IO.inspect() + end + + defp handle_method(%__MODULE__{operation: :load}, schema, params, opts) do + load(schema, params, opts) + end +end + +defimpl Inspect, for: Parameter.Engine do + import Inspect.Algebra + + def inspect(engine, opts) do + list = + for attr <- [:schema, :fields, :cast_fields, :changes, :errors, :data, :valid?] do + {attr, Map.get(engine, attr)} + end + + container_doc("#Parameter.Engine<", list, ">", opts, fn + {:schema, schema}, opts -> + concat("schema: ", to_doc(schema, opts)) + + {:fields, fields}, opts -> + concat("fields: ", fields(fields, opts)) + + {:cast_fields, cast_fields}, opts -> + concat("cast_fields: ", to_doc(cast_fields, opts)) + + {:changes, changes}, opts -> + concat("changes: ", to_doc(changes, opts)) + + {:data, data}, _opts -> + concat("data: ", to_doc(data, opts)) + + {:errors, errors}, opts -> + concat("errors: ", to_doc(errors, opts)) + + {:valid?, valid?}, opts -> + concat("valid?: ", to_doc(valid?, opts)) + end) + end + + # defp to_struct(%{__struct__: struct}, _opts), do: "#" <> Kernel.inspect(struct) <> "<>" + # defp to_struct(other, opts), do: to_doc(other, opts) + + defp fields(fields, opts) when is_list(fields) do + Enum.reduce(fields, [], fn %Parameter.Field{name: name}, acc -> + [name | acc] + end) + |> Enum.reverse() + |> to_doc(opts) + end + + defp fields(module, opts) do + to_doc(module, opts) + end +end diff --git a/lib/parameter/operator.ex b/lib/parameter/operator.ex deleted file mode 100644 index 328d912..0000000 --- a/lib/parameter/operator.ex +++ /dev/null @@ -1,148 +0,0 @@ -defmodule Parameter.Operator do - alias Parameter.Field - alias Parameter.Schema - - @type t :: %__MODULE__{ - schema: module() | nil, - valid?: boolean(), - data: map(), - changes: map(), - errors: map(), - cast_fields: list(atom()) - } - - defstruct schema: nil, - valid?: false, - data: nil, - changes: %{}, - errors: %{}, - cast_fields: [] - - @spec cast(module() | list(Field.t()), map(), list(atom())) :: t() - def cast(schema, params, cast_fields) do - %__MODULE__{ - schema: schema, - data: params, - cast_fields: cast_fields - } - end - - @spec cast(module() | list(Field.t()), map()) :: t() - def cast(schema, params) do - schema_fields = Schema.fields(schema) - - %__MODULE__{ - schema: schema, - data: params, - cast_fields: infer_cast_fields(schema_fields) - } - end - - def load( - %__MODULE__{ - schema: schema, - data: data, - cast_fields: cast_fields - } = operator, - opts \\ [] - ) do - schema_fields = Schema.fields(schema) - - fields_to_exclude = - schema_fields - |> Enum.map(& &1.name) - |> Enum.reject(fn field -> field in cast_fields end) - - opts_with_fields_to_exclude = Keyword.merge(opts, exclude: fields_to_exclude) - - case Parameter.load(schema, data, opts_with_fields_to_exclude) do - {:ok, loaded} -> - %__MODULE__{operator | valid?: true, changes: loaded} - |> load_assoc(opts) - - {:error, errors} -> - %__MODULE__{operator | valid?: false, errors: errors} - |> load_assoc(opts) - end - end - - def load_assoc(%__MODULE__{schema: schema, data: data} = operator, opts \\ []) do - schema_fields = Schema.fields(schema) - assoc_fields = Schema.assoc_fields(schema) - - Enum.reduce(assoc_fields, operator, fn assoc_field, operator -> - %Field{name: name, key: key, type: {assoc_type, schema}} = - Enum.find(schema_fields, &(&1.name == assoc_field.name)) - - opts = - if assoc_type == :array do - Keyword.merge(opts, many: true) - else - opts - end - - case schema |> cast(Map.get(data, key)) |> load(opts) do - %__MODULE__{valid?: true} = result -> - %__MODULE__{operator | changes: Map.put(operator.changes, name, result)} - - %__MODULE__{valid?: false} = result -> - %__MODULE__{operator | valid?: false, changes: Map.put(operator.changes, name, result)} - end - end) - end - - defp infer_cast_fields(fields) do - fields - |> Enum.filter(fn - %Parameter.Field{type: {:map, _nested}} -> false - %Parameter.Field{type: {:array, _nested}} -> false - _ -> true - end) - |> Enum.map(& &1.name) - end -end - -defimpl Inspect, for: Parameter.Operator do - import Inspect.Algebra - - def inspect(operator, opts) do - list = - for attr <- [:schema, :cast_fields, :changes, :errors, :data, :valid?] do - {attr, Map.get(operator, attr)} - end - - container_doc("#Parameter.Operator<", list, ">", opts, fn - {:schema, schema}, opts -> - concat("schema: ", schema_field(schema, opts)) - - {:cast_fields, cast_fields}, opts -> - concat("cast_fields: ", to_doc(cast_fields, opts)) - - {:changes, changes}, opts -> - concat("changes: ", to_doc(changes, opts)) - - {:data, data}, _opts -> - concat("data: ", to_doc(data, opts)) - - {:errors, errors}, opts -> - concat("errors: ", to_doc(errors, opts)) - - {:valid?, valid?}, opts -> - concat("valid?: ", to_doc(valid?, opts)) - end) - end - - # defp to_struct(%{__struct__: struct}, _opts), do: "#" <> Kernel.inspect(struct) <> "<>" - # defp to_struct(other, opts), do: to_doc(other, opts) - - defp schema_field(fields, opts) when is_list(fields) do - Enum.reduce(fields, [], fn %Parameter.Field{name: name}, acc -> - [name | acc] - end) - |> to_doc(opts) - end - - defp schema_field(module, opts) do - to_doc(module, opts) - end -end diff --git a/lib/parameter/schema.ex b/lib/parameter/schema.ex index 56cf473..f962d87 100644 --- a/lib/parameter/schema.ex +++ b/lib/parameter/schema.ex @@ -187,6 +187,7 @@ defmodule Parameter.Schema do {:ok, %{"level" => 0}} """ + alias Parameter.Field alias Parameter.Schema.Compiler alias Parameter.Types @@ -369,11 +370,21 @@ defmodule Parameter.Schema do end end + def module(module) when is_atom(module) do + module + end + + def module(fields) when is_list(fields) do + nil + end + def fields(module) when is_atom(module) do module.__param__(:fields) + rescue + _error -> module end - def fields(fields) when is_list(fields) do + def fields(fields) do fields end diff --git a/lib/parameter/schema/compiler.ex b/lib/parameter/schema/compiler.ex index 89b751a..26dc5c6 100644 --- a/lib/parameter/schema/compiler.ex +++ b/lib/parameter/schema/compiler.ex @@ -18,7 +18,8 @@ defmodule Parameter.Schema.Compiler do end def compile_schema!(schema) when is_atom(schema) do - schema + Parameter.Schema.fields(schema) + # schema end defp compile_type!({type, schema}) when is_tuple(schema) do