Object Validation with Assert
Tags: crystal, validation
Published: 2019-08-29 16:43:41

**UPDATE:** Oct 21, 2019 Update examples for `Assert` `v0.2.0`.

# Assert
[Assert](https://github.com/blacksmoke16/assert) is an annotation based object validation library heavily inspired by [Symfony Validation Constraint Annotations](https://symfony.com/doc/current/reference/constraints.html).

```crystal
@[Assert::NotBlank]
property name : String

@[Assert::GreaterThanOrEqual(value: 0)]
property age : Int32
```

## Introduction
Validations in Crystal, up until now, have mostly been shard specific.  One shard could have their own implementation for validating POST body data, while an ORM in the same application could be completely different.  `Assert` was initially included in [CrSerializer](https://github.com/blacksmoke16/CrSerializer) but has since gone through a rewrite and re-released as its own shard with the idea of brining some level of standardization that every shard could benefit from.  The main benefit of this would be the ability to use the same validations throughout your application.

This article is intended to be an overview of features/example use cases.  Most of the example code comes from the [API Docs](https://blacksmoke16.github.io/assert/Assert.html) which contains more detailed documentation on each feature.

## Features
`Assert` was created with flexibility and extensibility in mind, by default it includes:

* Multitude of built-in assertions:
  * Email
  * Ip
  * URL
  * Choice
  * Sub object/array of objects are all valid
  * [Etc.](https://blacksmoke16.github.io/assert/Assert/Assertions.html)
* Ability to create/use custom assertions
  * Use generics within custom assertions
* Ability to run a subset of assertions on an object
* Control how and when the object is validated

### Usage
In order to use `Assert`:
* Add it to your `shard.yml`
* Require it - `require "assert"`
* Include it in your object - `include Assert`

An [example usage](https://blacksmoke16.github.io/assert/Assert.html) can be seen in the API docs.

### Groups
Each assertion can also be assigned to a group(s) in order to run subsets of assertions.

```crystal
class Groups
  include Assert

  def initialize(@group_1 : Int32, @group_2 : Int32, @default_group : Int32); end

  @[Assert::EqualTo(value: 100, groups: ["group1"])]
  property group_1 : Int32

  @[Assert::EqualTo(value: 200, groups: ["group2"])]
  property group_2 : Int32

  @[Assert::EqualTo(value: 300)]
  property default_group : Int32
end

Groups.new(100, 200, 300).valid?                      # => true
Groups.new(100, 100, 100).valid?                      # => false
Groups.new(100, 100, 100).valid?(["group1"])          # => true
Groups.new(200, 100, 300).valid?(["default"])         # => true
Groups.new(100, 200, 200).valid?("group1", "default") # => false
```

This allows you to reuse the same class/struct in various scenarios since you can control which assertions run.  For example, running a different set of assertions when a user registers for the first time, versus when they update some of their information.

### Custom Assertions & Generics

If your application has some unique validation requirements that the included assertions do not cover; you can create custom assertions.

A custom assertion is simply a class that inherits from `Assert::Assertions::Assertion`, applies the `Assert::Assertions::Register` annotation, and implements some methods.

```crystal
@[Assert::Assertions::Register(annotation: Assert::Exists)]
# A custom assertion that validates if a record exists with the given *id*.
#
# For example, an ORM model where `.exists?` checks if a record exists with the given PK.
# I.e. `SELECT exists(select 1 from "users" WHERE id = 123);`
class Exists(PropertyType, Model) < Assert::Assertions::Assertion
  # This is a helper macro to make defining the initialize method easier
  initializer(
    actual: PropertyType
  )

  # :inherit:
  def default_message_template : String
    "'%{actual}' is not a valid %{property_name}."
  end

  # :inherit:
  def valid? : Bool
    Model.exists? @actual
  end
end
```

In this example, we're defining a custom assertion called `Exists` with the following methods:

* initializer - A helper macro that defines the initializer.  
  * See the [API Docs](https://blacksmoke16.github.io/assert/Assert/Assertions/Assertion.html) for more information on what the initializer is used for.
  * See [Assertion.initializer](https://blacksmoke16.github.io/assert/Assert/Assertions/Assertion.html#initializer(**ivars)-macro) for more information on the macro itself
* default_message_template - The error message template to use if the assertion fails and no custom message was provided.
  * Instance variables on the assertion can be used within the template by surrounding the ivar's name in double curly braces.
* valid? - Implements the logic that determines if the property is valid or not.
  * The implementation could be whatever you wish as long as it returns `true` or `false`.

We also need to apply the `Assert::Assertions::Register` annotation, which is used to define the name of the annotation that should be applied to properties.  In this case `Assert::Exists`.  Now this assertion is ready to be used.

```crystal
class Post < SomeORM::Model
  include Assert

  def initialize; end

  @[Assert::Exists(User)]
  property author_id : Int64 = 17
end
```

In this example, the assertion would run a SQL query to determine if a `User` with an *id* of 17 exists. 

A Dependency Injection shard, such as [Athena's DI Module](https://github.com/Blacksmoke16/athena) could also be used to inject the current user/request, or some ACL service into the assertion.

## Example Use Cases

`Assert` is not useful unless there are objects that need validating.  For some projects/applications it might not be necessary.  However web frameworks and ORMs could easily benefit, as validations are an important part of both.

### Web Framework

Web applications have the most apparent need for validations.  This could either be validating the POST body in an API, or validating user input from a form submission.  However, `Assert` is not for client side validation.  It would not, for example, prevent a user from entering a string in a numeric form field.  It _would_ be caught when that form was submitted, and the server runs the validations.

Lets go over an example of how `Assert` could be used within an API using Kemal.

```crystal
require "kemal"
require "assert"
require "json"

# user.cr
class User
  include JSON::Serializable
  include Assert

  # Asserts that their age is >= 0 AND not nil
  @[Assert::NotNil]
  @[Assert::GreaterThanOrEqual(value: 0)]
  property age : Int32?

  # Assert their name is not blank
  @[Assert::NotBlank]
  property name : String

  # Assert their email is not blank AND is a valid format
  @[Assert::Email(message: "'%{actual}' is not a proper email")]
  @[Assert::NotBlank]
  property email : String

  # Assert their password is between 7 and 25 characters
  @[Assert::Size(Range(Int32, Int32), range: 7..25)]
  property password : String

  # Have the object be validated after the it is deserialized
  def after_initialize
    validate!
  end
end

# users_controller.cr
post "/users" do |env|
  env.response.content_type = "application/json"
  user = User.from_json env.request.body.not_nil!
  # Do stuff with a valid user
  
  # Return the user as JSON in the response
  user.to_json
rescue ex : Assert::Exceptions::ValidationError
  env.response.status_code = 400
  env.response.print ex.to_json
end

Kemal.run  
```

This setup makes the validations on `User` run after the object has been deserialized from the JSON POST body.  If the object is not valid `#validate!` will raise a `ValidationError` exception. We are catching that exception and returning the JSON error message back to the user.

`Assert` also defines `#valid?` and `#validate` methods which return a `Bool` if it the object is valid, or an array of failed assertions respectively.  The former being most useful if you just want to know if an object is valid, with the latter being most useful if you wanted to do something with the assertion data, such as formatting an error message.

```bash
curl -X POST \
  http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{
	"name": "Jim",
	"age": -1,
	"email": "foobar",
	"password": "monkey123"
}'
```

Would return the following error

```json
{
    "code": 400,
    "message": "Validation tests failed",
    "errors": [
        "'age' should be greater than or equal to '0'",
        "'foobar' is not a proper email"
    ]
}
```

The JSON error response format can be changed by overriding the [#to_json(builder : JSON::Builder)](https://blacksmoke16.github.io/assert/Assert/Exceptions/ValidationError.html#to_json(builder:JSON::Builder):Nil-instance-method) method within the exception class; if you wanted to group the errors by the property name for example.

```crystal
class Assert::Exceptions::ValidationError
  def to_json(builder : JSON::Builder)
    builder.object do
      builder.field "code", 400
      builder.field "message", @message
      builder.field "errors" do
        builder.object do
          @failed_assertions.group_by(&.property_name).each do |prop, errors|
            builder.field prop, errors.map &.message
          end
        end
      end
    end
  end
end
```

Would output:

```json
{
    "code": 400,
    "message": "Validation tests failed",
    "errors": {
        "age": [
            "'age' should be greater than or equal to '0'"
        ],
        "email": [
            "'foobar' is not a proper email"
        ]
    }
}
```

This, of course, is just a demonstration.  A framework could easily integrate `Assert` to make the process more streamlined.  Such as how its used within [Athena](https://github.com/blacksmoke16/athena).

```crystal
@[Athena::Routing::Post(path: "users")]
@[Athena::Routing::ParamConverter(param: "body", type: User, converter: Athena::Routing::Converters::RequestBody)]
def new_user(body : User) : User
  body
end
```

The `ParamConverter` handles converting the request body into a `User` object, any validations on `User` will also be executed after it has been deserialized.  The JSON error will be returned automatically if it is not valid.  The controller action will also never execute if the object is not valid.

While using `Assert` may make your objects larger, it removes the need for validation within the controller code as you know can be assured that invalid objects will not make it far.  It also allows you to share the same validation logic from other parts of your application.  Such as an ORM.

### ORM Models

ORMs models inherently require validation to make sure what is being persisted to the database is correct.  Continuing along with our `Kemal` example, we could easily convert our `User` object into a `Granite` model.

> **NOTE:** This is just an example and does not include setting up the ORM

```crystal
require "granite"
require "assert"

class User < Granite::Base
  include Assert
  
  table "users"
  
  column id : Int64?, primary: true

  # Asserts that their age is >= 0 AND not nil
  @[Assert::NotNil]
  @[Assert::GreaterThanOrEqual(value: 0)]
  column age : Int32?

  # Assert their name is not blank
  @[Assert::NotBlank]
  column name : String

  # Assert their email is not blank AND is a valid format
  @[Assert::Email(message: "'%{actual}' is not a proper email")]
  @[Assert::NotBlank]
  column email : String

  # Assert their password is between 7 and 25 characters
  @[Assert::Size(Range(Int32, Int32), range: 7..25)]
  column password : String

  # Granite includes `JSON::Serializble` by default
  def after_initialize
    validate!
  end
end
```

Our controller action would also slightly change.

```crystal
post "/users" do |env|
  env.response.content_type = "application/json"
  user = User.from_json env.request.body.not_nil!
  # We can now just save the user since we know its valid
  user.save
  
  # Return the user as JSON in the response
  user.to_json
rescue ex : Assert::Exceptions::ValidationError
  env.response.status_code = 400
  env.response.print ex.to_json
end
```

POSTing a valid user would return:

```json
{
    "id": 1,
    "age": 17,
    "name": "Jim",
    "email": "test@gmail.com",
    "password": "monkey123"
}
```

### Conclusion

`Assert`'s benefits:

1. Having a framework agnostic validation library that can be used across shards.
   * Sharing validation logic between your API controllers and ORM models for example.
2. Easily extensible by the user.
   * Custom assertions can be easily defined without needing to edit any shard's source code.
3. Easy to integrate into existing frameworks/shards.
   * Current projects/frameworks could easily add `Assert` to their objects and control when/how they get validated.
     * Such as the `Kemal` example.  Moving validations into the objects and out of the controller actions
     * Or an ORM that would make sure the model is valid before saving

As usual, if you have any questions, feedback, or ideas for new assertions; feel free to message me on the [Crystal Gitter](https://gitter.im/crystal-lang/crystal), or create an issue on the [Github Repo](https://github.com/blacksmoke16/assert).