if Code.ensure_loaded?(Ecto) do defmodule EView.Helpers.ChangesetValidationsParser do @moduledoc false # This module converts changeset to a error structure described in API Manifest. @entry_type "json_data_property" @jsonpath_root "$" @jsonpath_joiner "." def changeset_to_rules(%Ecto.Changeset{} = changeset, entry_type \\ @entry_type) do changeset |> Ecto.Changeset.traverse_errors(&construct_rule/3) |> Enum.flat_map(&errors_flatener(&1, @jsonpath_root, entry_type)) end defp construct_rule(%Ecto.Changeset{validations: validations}, field, {message, opts}) do validation_name = opts[:validation] get_rule(field, validation_name, put_validation(validations, validation_name, field, opts), message, opts) end # Special case for cast validation that stores type in field that dont match validation name defp put_validation(validations, :cast, field, opts) do [{field, {:cast, opts[:type]}} | validations] end # Special case for metadata validator that can't modify to changeset validations defp put_validation(validations, :length, field, opts) do validation = Keyword.take(opts, [:min, :max, :is]) [{field, {:length, validation}} | validations] end # Embeds defp put_validation(validations, :embed, field, opts) do [{field, {:cast, Keyword.get(opts, :type)}} | validations] end defp put_validation(validations, _, _field, _opts), do: validations # Embeds defp get_rule(field, _validation_name, validations, message, validation: :embed, type: type) do validation_name = :cast get_rule(field, validation_name, validations, message, validation: :cast, type: type) end defp get_rule(field, validation_name, validations, message, opts) do %{ description: message |> get_rule_description(opts), rule: opts[:validation], params: field |> reduce_rule_params(validation_name, validations) |> cast_rules_type() } end defp get_rule_description(message, opts) do Enum.reduce(opts, message, fn # Lists {key, value}, acc when is_list(value) -> String.replace(acc, "%{#{key}}", Enum.join(value, ", ")) {key, {:array, :string}}, acc -> String.replace(acc, "%{#{key}}", "string") {key, {:array, :map}}, acc -> String.replace(acc, "%{#{key}}", "map") {key, {:array, Ecto.UUID}}, acc -> String.replace(acc, "%{#{key}}", "uuid") # Everything else is a string {key, value}, acc -> String.replace(acc, "%{#{key}}", to_string(value)) end) end defp reduce_rule_params(field, validation_name, validations) do validations |> Enum.reduce([], fn {^field, v}, acc -> [v | acc] _, acc -> acc end) |> Enum.reverse() |> Enum.reduce([], fn # Validation with keywords {^validation_name, [h | _] = keyword}, acc when is_tuple(h) -> keyword ++ acc # With list {^validation_name, list}, acc when is_list(list) -> list ++ acc # With regex pattern {^validation_name, %Regex{} = regex}, acc -> [inspect(regex) | acc] # Ecto.UUID rule {^validation_name, {:array, Ecto.UUID}}, acc -> [:uuid | acc] # Array of anything # TODO: Return ID of element from Ecto {^validation_name, {:array, type}}, acc when is_atom(type) -> type = type |> Atom.to_string() |> Kernel.<>("s_array") |> String.to_atom() [type | acc] # Or at least parseable {^validation_name, rule_description}, acc -> [rule_description | acc] # Skip rest _, acc -> acc end) |> Enum.uniq() end defp cast_rules_type([h | _] = rules) when is_tuple(h), do: rules |> Enum.into(%{}) defp cast_rules_type(rules), do: rules # Recursively flatten errors map defp errors_flatener({field, [%{rule: _} | _] = rules}, prefix, entry_type) when is_list(rules) do [ %{ entry_type: entry_type, entry: entry_path(entry_type, prefix, field), rules: rules } ] end defp errors_flatener({field, errors}, prefix, entry_type) when is_map(errors) do Enum.flat_map(errors, &errors_flatener(&1, entry_path(entry_type, prefix, field), entry_type)) end defp errors_flatener({field, errors}, prefix, entry_type) when is_list(errors) do {acc, _} = errors |> Enum.reduce({[], 0}, fn inner_errors, {acc, i} -> inner_rules = inner_errors |> Enum.flat_map( &errors_flatener( &1, entry_path(entry_type, prefix, field) <> "[#{i}]", entry_type ) ) {acc ++ inner_rules, i + 1} end) acc end defp entry_path("json_data_property", prefix, field) do prefix <> @jsonpath_joiner <> to_string(field) end defp entry_path(_entry_type, _prefix, field) do to_string(field) end end end