Dredd

Dredd judges data for you. It's a validator for a wide range of possibilities.

datastructures. Started as a frok of Justify it's been rewritten to handle plain values as well as deeply nested structures and return understandable error-structures.

Following in the footsteps of Ecto.Changeset, Dredd allows you to pipe values into a series of validation functions using a simple and familiar API. No schemas or casting required.

Please see the Changelog for what has changed in the latest release.

a-basic-example

A Basic Example

iex> Dredd.validate_number("this is not a number", :integer)
%Dredd.Dataset{
  data: "this is not a number",
  error: %Dredd.SingleError{
    validator: :number,
    message: "has incorrect numerical type",
    metadata: %{kind: :integer}
  },
  valid?: false
}

As you can see this is a rather verbose output; but we believe that this can come in handy in conjunction when you have to translate the error on the user interface.

Each validation function will return a Dredd.Dataset which can be passed into the next function. If a validation error is encountered the dataset will be marked as invalid and an error will be added to the struct. Each validator behaves in a fail-fast manner. If the Dataset is already invalid no further validation happens for any given value. As a tip: you should order your validations by descending importance.

Errors are distinguished by type to allow traversal of nested structures by matching against the struct type of the encountered error.

When validating single values a Dredd.SingleError is returned whenever it fails.

validating-lists

Validating Lists

To ramp up the complexits: you can validate all elements of a list by handing a validation function to Dredd.validate_list/3.

simple-list-example

Simple List Example

This is how you would validate a list of strings.

iex(2)> Dredd.validate_list(["string", -1, "string", 0], &Dredd.validate_string/1)
%Dredd.Dataset{
  data: ["string", -1, "string", 0],
  error: %Dredd.ListErrors{
    validator: :list,
    errors: %{
      1 => %Dredd.SingleError{
        validator: :string,
        message: "is not a string",
        metadata: %{kind: :type}
      },
      3 => %Dredd.SingleError{
        validator: :string,
        message: "is not a string",
        metadata: %{kind: :type}
      }
    }
  },
  valid?: false
}

If the list does not match given length requirements or the given data is not a list, the returned Dredd.Dataset will be invalid and contain a Dredd.SingleError.

In case an element of the list is invalid, the returned Dredd.Dataset will contain Dredd.ListErrors with its errors field containing a map. The keys of that map are the indices of the values for which validation failed. The values of that map are Dredd.SingleError, Dredd.ListErrors or Dredd.MapErrors depending on the given validator for the list items.

This distinction is meant to help with parsing the errors of nested validations.

complex-list-example

Complex List Example

You can exploit the fact that cou can pipe the output of all validator functions into the next one and compose chain together multiple values for the elements of your list.

iex> item_validator = fn data ->
...>   Dredd.validate_string(data)
...>   |> Dredd.validate_email()
...> end
iex> Dredd.validate_list(["foo", "foo@bar.com"], item_validator)
%Dredd.Dataset{
  data: ["foo", "foo@bar.com"],
  error: %Dredd.ListErrors{
    validator: :list,
    errors: %{
      0 => %Dredd.SingleError{
        validator: :email,
        message: "is not a valid email address",
        metadata: %{}
      }
    }
  },
  valid?: false
}

handling-non-lists

Handling Non-Lists

In case Dredd.validate_list/3 is handed anything but a list, it will return a Dredd.SingleError indicating that the value is of wrong type:

iex> Dredd.validate_list(100, &Dredd.validate_email/1)
%Dredd.Dataset{
  data: 100,
  error: %Dredd.SingleError{
    validator: :list,
    message: "is not a list",
    metadata: %{kind: :type}
  },
  valid?: false
}

validating-structs

Validating Structs

You can validate fields of a struct or map using the Dredd.validate_map/2 function.

simple-struct-example

Simple Struct Example

iex> value = %{ field_a: 10, field_b: "foo" }
%{field_a: 10, field_b: "foo"}
iex> validator_map = %{
...>   field_a: &Dredd.validate_string/1,
...>   field_b: fn data -> Dredd.validate_number(data, :integer) end
...>}
iex> Dredd.validate_map(value, validator_map)
%Dredd.Dataset{
  data: %{field_a: 10, field_b: "foo"},
  error: %Dredd.MapErrors{
    validator: :map,
    errors: %{
      field_a: %Dredd.SingleError{
        validator: :string,
        message: "is not a string",
        metadata: %{kind: :type}
      },
      field_b: %Dredd.SingleError{
        validator: :number,
        message: "has incorrect numerical type",
        metadata: %{kind: :integer}
      }
    }
  },
  valid?: false
}

In case of errors the output of Dredd.validate_map\3 is quite similar to that of Dredd.validate_list\3.

If the give value is not a valid map the Dredd.Dataset will contain a Dredd.SingleError.

If the validations on field-level failed the Dredd.Dataset will contain Dredd.MapErrors. The keys in that map are the fieldnames of the invalid fields. The values in the map can be Dredd.SingleError, Dredd.ListErrors or Dredd.MapErrors depending on the valdiator of that field.

This distinction is meant to help with parsing the errors of nested validations.

nested-example

Nested Example

iex> email_list_validator = fn data ->
...>   Dredd.validate_list(data, &Dredd.validate_email/1)
...> end
# Function<...>
iex> validate_embedded_map = fn data ->
...>   validator_map = %{
...>     email_list: email_list_validator,
...>     number: &Dredd.validate_number(&1, :integer)
...>   }
...>   Dredd.validate_map(data, validator_map)
...> end
# Function<...>
iex> map_list_validator = fn data ->
...>   Dredd.validate_list(data, validate_embedded_map)
...> end
# Function<...>
iex> validator_map = %{
...>   map_list: map_list_validator,
...>   str_field: &Dredd.validate_string/1
...> }
%{
  map_list: #Function<...>,
  str_field: &Dredd.validate_string/1
}
iex> value = %{
...>   map_list: [
...>    %{email_list: ["foo@bar.com", "bang@baz.net"], number: 10 },
...>    %{email_list: ["foo@bar.com", "blubb"], number: 20 }
...>   ],
...>   str_field: "bar"
...> }
%{
  map_list: [
    %{email_list: ["foo@bar.com", "bang@baz.net"], number: 10},
    %{email_list: ["foo@bar.com", "blubb"], number: 20}
  ],
  str_field: "bar"
}
iex> Dredd.validate_map(value, validator_map)
%Dredd.Dataset{
  data: %{
    map_list: [
      %{email_list: ["foo@bar.com", "bang@baz.net"], number: 10},
      %{email_list: ["foo@bar.com", "blubb"], number: 20}
    ],
    str_field: "bar"
  },
  error: %Dredd.MapErrors{
    validator: :map,
    errors: %{
      map_list: %Dredd.ListErrors{
        validator: :list,
        errors: %{
          1 => %Dredd.MapErrors{
            validator: :map,
            errors: %{
              email_list: %Dredd.ListErrors{
                validator: :list,
                errors: %{
                  1 => %Dredd.SingleError{
                    validator: :email,
                    message: "is not a valid email address",
                    metadata: %{}
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  valid?: false
}

In the example above, the second email in the list of the second sub-map in the map_list field is invalid. Even with all the extra information the output is quite confusing. That's the primary reason we've added all the extra type information. With that you can pattern match against the struct type and have a fighting chance at successfully traversing the data.

Copyright (c) 2022 Marcus Autenrieth

Dredd is licensed under the MIT License, see LICENSE.md for details.