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

Assert

Assert is an annotation based object validation library heavily inspired by Symfony Validation Constraint Annotations.

```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 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 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.
  • 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 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 for more information on what the initializer is used for.
  • See Assertion.initializer 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 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) 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.

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.
  2. Sharing validation logic between your API controllers and ORM models for example.
  3. Easily extensible by the user.
  4. Custom assertions can be easily defined without needing to edit any shard's source code.
  5. Easy to integrate into existing frameworks/shards.
  6. 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, or create an issue on the Github Repo.