# Copyright 2023 Arkemis S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Arke.Validator do
@moduledoc """
This module provide validation before assign a certain value to an `{arke_struct}`
"""
alias Arke.Boundary.{ArkeManager, ParameterManager}
alias Arke.QueryManager, as: QueryManager
alias Arke.Utils.ErrorGenerator, as: Error
alias Arke.DatetimeHandler, as: DatetimeHandler
alias Arke.Core.{Arke, Unit, Parameter}
@type func_return() :: {:ok, Unit.t()} | Error.t()
@doc """
Function to check the given data based on the fields in the reference schema.
## Parameters
- unit => => %Arke.Core.Unit{} => unit to add
- persistence_fn => fun.() => function containng the action that will be performed on the Repo
- project => :atom => identify the `Arke.Core.Project`
## Example
iex> schema = Arke.Core.Arke.new
...> param = Arke.Core.Parameter.new(%{type: :string,opts: [id: :name]})
...> schema = Arke.Core.Arke.add_parameter(schema, param)
...> Arke.Validator.validate(%{arke: schema, data: %{name: "test"}})
## Return
%{:ok,_}
%{:error, [message]}
"""
@spec validate(unit :: Unit.t(), peristence_fn :: (() -> any()), project :: atom()) ::
func_return()
def validate(%{arke_id: arke_id} = unit, persistence_fn, project \\ :arke_system) do
with {:ok, unit} <- check_duplicate_unit(unit, project, persistence_fn) do
{%{data: data} = unit, errors} = before_validate(unit, project)
%{data: arke_data} = arke = ArkeManager.get(arke_id, project)
unit_parameters =
Enum.filter(ArkeManager.get_parameters(arke), fn %{data: %{persistence: persistence}} ->
persistence == "arke_parameter"
end)
res =
Enum.reduce(unit_parameters, {unit, errors}, fn p, {new_unit, errors} = _res ->
{value, err} = validate_parameter(arke, p, Unit.get_value(data, p.id), project)
{Unit.update(new_unit, [{p.id, value}]), errors ++ err}
end)
{new_unit, errors} = res
filtered_errors = check_old_values(errors, data, new_unit.data, persistence_fn)
get_result({new_unit, filtered_errors})
else
{:error, errors} -> get_result({unit, errors})
end
# TODO: handle after validate
end
defp before_validate(%{arke_id: arke_id} = unit, project) do
arke = ArkeManager.get(arke_id, project)
with {:ok, unit} <- ArkeManager.call_func(arke, :before_validate, [arke, unit]),
do: {unit, []},
else: ({:error, errors} -> {unit, errors})
end
defp check_duplicate_unit(%{id: unit_id} = unit, _project, :create) when is_nil(unit_id),
do: {:ok, unit}
defp check_duplicate_unit(%{id: unit_id} = unit, project, :create) do
with nil <- QueryManager.get_by(%{:id => unit_id, :project => project}),
do: {:ok, unit},
else: (_ -> {:error, [{"You can not create Unit with same id", unit_id}]})
end
defp check_duplicate_unit(%{id: unit_id} = unit, _project, _persistence_fn), do: {:ok, unit}
defp check_old_values(errors, old_unit_data, new_unit_data, :update) do
duplicate_list =
Enum.filter(errors, fn {k, v} ->
is_binary(k) and String.contains?(k, "duplicate") and is_atom(v)
end)
same_value =
Enum.filter(duplicate_list, fn {_k, v} -> old_unit_data[v] == new_unit_data[v] end)
:ordsets.subtract(errors, same_value)
end
defp check_old_values(errors, _old_unit_data, _new_unit_data, :create), do: errors
defp get_result({_unit, errors} = _res) when is_list(errors) and length(errors) > 0,
do: Error.create(:parameter_validation, errors)
defp get_result({unit, _errors} = _res), do: {:ok, unit}
@doc """
Check if the value can be assigned to a given parameter in a specific schema struct.
## Parameters
- schema_struct => %{arke_struct} => the element where to find and check the field
- field => :atom => the id of the paramater
- value => any => the value we want to assign to the above field
- project => :atom => identify the `Arke.Core.Project`
## Example
iex> Arke.Boundary.ArkeValidator.validate_field(schema_struct, :field_id, value_to_check)
## Returns
{value,[]} if success
{value,["parameter label", message ]} in case of error
"""
@spec validate_parameter(
arke :: Arke.t(),
parameter :: Sring.t() | atom() | Parameter.parameter_struct(),
value :: String.t() | number() | atom() | boolean() | map() | list(),
project :: atom()
) :: func_return()
def validate_parameter(arke, parameter, value, project \\ :arke_system)
def validate_parameter(arke, parameter, value, project) when is_atom(parameter) do
parameter = get_parameter(arke, parameter, project)
check_parameter(parameter, value, project)
end
defp get_parameter(nil, parameter_id, project), do: ParameterManager.get(parameter_id, project)
defp get_parameter(arke, parameter_id, project),
do: ArkeManager.get_parameter(arke, parameter_id)
def validate_parameter(_arke, parameter, value, project) do
check_parameter(parameter, value, project)
end
defp check_parameter(parameter, value, project) do
value = get_default_value(parameter, value)
value = parse_value(parameter, value)
errors =
[]
|> check_required_parameter(parameter, value)
|> check_by_type(parameter, value)
|> check_duplicate(parameter, value, project)
{value, errors}
end
def get_default_value(parameter, value) when is_nil(value), do: handle_default_value(parameter)
def get_default_value(parameter, value), do: value
defp parse_value(%{arke_id: :integer} = _, value)
when not is_integer(value) and not is_nil(value) do
case Integer.parse(value) do
:error -> value
{v, ""} -> v
{v, e} -> value
end
end
defp parse_value(%{arke_id: :float} = _, value)
when not is_number(value) and not is_nil(value) do
case Float.parse(value) do
:error -> value
{v, ""} -> v
{v, e} -> value
end
end
defp parse_value(_, value), do: value
defp handle_default_value(%{arke_id: :string, data: %{default_string: default_string}} = _),
do: default_string
defp handle_default_value(%{arke_id: :integer, data: %{default_integer: default_integer}} = _),
do: default_integer
defp handle_default_value(%{arke_id: :float, data: %{default_float: default_float}} = _),
do: default_float
defp handle_default_value(%{arke_id: :boolean, data: %{default_boolean: default_boolean}} = _),
do: default_boolean
defp handle_default_value(%{arke_id: :date, data: %{default_date: default_date}} = _),
do: default_date
defp handle_default_value(%{arke_id: :time, data: %{default_time: default_time}} = _),
do: default_time
defp handle_default_value(
%{arke_id: :datetime, data: %{default_datetime: default_datetime}} = _
),
do: default_datetime
defp handle_default_value(%{arke_id: :dict, data: %{default_dict: default_dict}} = _),
do: default_dict
defp handle_default_value(%{arke_id: :link, data: %{default_link: default_link}} = _),
do: default_link
defp handle_default_value(_), do: nil
defp check_required_parameter(errors, %{id: id, data: %{required: true}} = _parameter, value)
when is_nil(value),
do: errors ++ [{id, "is required"}]
defp check_required_parameter(errors, _parameter, _value), do: errors
defp check_duplicate(errors, %{id: id, data: %{unique: true}} = _parameter, value, project) do
with nil <- QueryManager.get_by(%{id => value, :project => project}),
do: errors,
else: (_ -> errors ++ [{"duplicate values are not allowed for", id}])
end
defp check_duplicate(errors, _parameter, _value, _project), do: errors
defp check_by_type(errors, _parameter, value) when is_nil(value), do: errors
######################################################################
# STRING PARAMETER ###################################################
######################################################################
defp check_by_type(errors, %{arke_id: :string} = parameter, value) when is_atom(value),
do: check_by_type(errors, parameter, Atom.to_string(value))
defp check_by_type(errors, %{arke_id: :string} = parameter, value)
when is_binary(value) or is_atom(value) or is_list(value) do
errors
|> check_max_length(parameter, value)
|> check_min_length(parameter, value)
|> check_values(parameter, value)
end
defp check_by_type(errors, %{arke_id: :string} = parameter, _value),
do: errors ++ [{parameter.data.label, "must be a string"}]
defp check_values(errors, %{data: %{values: nil}} = _parameter, _value), do: errors
defp check_values(
errors,
%{arke_id: type, data: %{values: values, label: label}} = parameter,
value
)
when is_list(value) do
admitted_values = Enum.map(values, fn %{label: _l, value: v} -> v end)
with true <- check_values_type(value, type) do
with [] <- value -- admitted_values do
errors
else
_ -> __enum_error_common__(errors, parameter)
end
else
_ -> errors ++ [{value, "#{label} must be a list of #{to_string(type)}}"}]
end
end
defp check_values(errors, %{data: %{values: values}} = parameter, value) do
case Enum.find(values, fn %{label: _l, value: v} -> v == value end) do
nil -> __enum_error_common__(errors, parameter)
_ -> errors
end
end
defp check_values(
errors,
%{arke_id: type, data: %{multiple: true, values: _values, label: label}} = parameter,
value
),
do: errors ++ [{value, "#{label} must be a list of #{type}}"}]
defp check_values_type(value, type) do
condition =
cond do
type == :string -> fn v -> is_binary(v) end
type == :integer -> fn v -> is_integer(v) end
type == :float -> fn v -> is_number(v) end
end
Enum.all?(value, &condition.(&1))
end
defp check_max_length(errors, %{data: %{max_length: max_length}} = parameter, _)
when is_nil(max_length),
do: errors
defp check_max_length(
errors,
%{data: %{label: label, max_length: max_length}} = parameter,
value
) do
if String.length(value) > max_length do
errors ++ [{label, "max length is #{max_length}"}]
else
errors
end
end
defp check_min_length(errors, %{data: %{min_length: min_length}} = parameter, _)
when is_nil(min_length),
do: errors
defp check_min_length(
errors,
%{data: %{label: label, min_length: min_length}} = parameter,
value
) do
if String.length(value) < min_length do
errors ++ [{label, "min length is #{min_length}"}]
else
errors
end
end
######################################################################
# NUMBER PARAMETER ###################################################
######################################################################
defp check_by_type(errors, %{arke_id: :integer} = parameter, value)
when is_integer(value) or is_list(value) do
errors
|> check_max(parameter, value)
|> check_min(parameter, value)
|> check_values(parameter, value)
end
defp check_by_type(errors, %{arke_id: :integer, data: %{label: label}} = parameter, _value),
do: errors ++ [{label, "must be an integer"}]
defp check_by_type(errors, %{arke_id: :float} = parameter, value)
when is_number(value) or is_list(value) do
errors
|> check_max(parameter, value)
|> check_min(parameter, value)
|> check_values(parameter, value)
end
defp check_by_type(errors, %{arke_id: :float, data: %{label: label}} = parameter, _value),
do: errors ++ [{label, "must be a float"}]
defp check_by_type(_errors, %{arke_id: :dict} = _parameter, value) when is_map(value), do: []
defp check_by_type(errors, %{arke_id: :dict, data: %{label: label}} = parameter, _value),
do: errors ++ [{label, "must be a map"}]
defp check_by_type(_errors, %{arke_id: :list} = _parameter, value) when is_list(value), do: []
defp check_by_type(errors, %{arke_id: :list, data: %{label: label}} = parameter, _value),
do: errors ++ [{label, "must be a list"}]
defp check_max(errors, %{data: %{max: max}} = parameter, _) when is_nil(max), do: errors
defp check_max(errors, %{data: %{max: max, label: label}} = parameter, value) do
if value > max do
errors ++ [{label, "max is #{max}"}]
else
errors
end
end
defp check_min(errors, %{data: %{min: min}} = parameter, _) when is_nil(min), do: errors
defp check_min(errors, %{data: %{min: min, label: label}} = parameter, value) do
if value < min do
errors ++ [{label, "min is #{min}"}]
else
errors
end
end
# DATE
defp check_by_type(errors, %{arke_id: :date} = parameter, value) do
case DatetimeHandler.parse_date(value) do
{:ok, _date} -> errors
{:error, msg} -> errors ++ [{parameter.data.label, msg}]
end
end
# TIME
defp check_by_type(errors, %{arke_id: :time} = parameter, value) do
case DatetimeHandler.parse_time(value) do
{:ok, _date} -> errors
{:error, msg} -> errors ++ [{parameter.data.label, msg}]
end
end
# DATETIME
defp check_by_type(errors, %{arke_id: :datetime} = parameter, value) do
case DatetimeHandler.parse_datetime(value) do
{:ok, _date} -> errors
{:error, msg} -> errors ++ [{parameter.data.label, msg}]
end
end
######################################################################
# ARKE LINK PARAMETER ################################################
######################################################################
# defp check_by_type(errors, %{arke_id: :arke_link} = parameter, value, project) when is_nil(value),
# do: check_by_type(errors, parameter, [], project)
# defp check_by_type(errors, %{arke_id: :arke_link} = parameter, value, project) when is_list(value)
# units = QueryManager.filter_by(project: project, id__in: value)
# with nil <- QueryManager.filter_by(project: project, id__in: value),
# do: errors,
# else: (_ -> errors ++ [{"duplicate values are not allowed for", id}])
# end
defp check_by_type(errors, _, _), do: errors
defp __enum_error_common__(errors, %{id: id, data: %{values: nil}} = _parameter), do: errors
defp __enum_error_common__(errors, %{id: id, data: %{values: %{}}} = _parameter), do: errors
defp __enum_error_common__(errors, %{id: id, data: %{values: []}} = _parameter), do: errors
defp __enum_error_common__(errors, %{id: id, data: %{values: values}} = _parameter) do
errors ++
[{"allowed values for #{id} are", Enum.map(values, fn %{label: _l, value: v} -> v end)}]
end
end