• Stars
    star
    29
  • Rank 860,307 (Top 17 %)
  • Language
    Crystal
  • License
    Other
  • Created almost 5 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

💎 Data validation module for Crystal lang

validator

CI Status GitHub release Docs

∠(・.-)―〉 →◎ validator is a Crystal data validation module.
Very simple and efficient, all validations return true or false.

Also validator/check (not exposed by default) provides:

  • Error message handling intended for the end user.
  • Also (optional) a powerful and productive system of validation rules. With self-generated granular methods for cleaning and checking data.

Validator respects the KISS principle and the Unix Philosophy. It's a great basis tool for doing your own validation logic on top of it.

Installation

  1. Add the dependency to your shard.yml:
dependencies:
  validator:
    github: nicolab/crystal-validator
  1. Run shards install

Usage

There are 3 main ways to use validator:

  • As a simple validator to check rules (eg: email, url, min, max, presence, in, ...) which return a boolean.
  • As a more advanced validation system which will check a series of rules and returns all validation errors encountered with custom or standard messages.
  • As a system of validation rules (inspired by the Laravel framework's Validator) which makes data cleaning and data validation in Crystal very easy! With self-generated granular methods for cleaning and checking data of each field.

By default the validator module expose only Validator and Valid (alias) in the scope:

require "validator"

Valid.email? "[email protected]" # => true
Valid.url? "https://github.com/Nicolab/crystal-validator" # => true
Valid.my_validator? "value to validate", "hello", 42 # => true

An (optional) expressive validation flavor, is available as an alternative. Not exposed by default, it must be imported:

require "validator/is"

is :email?, "[email protected]" # => true
is :url?, "https://github.com/Nicolab/crystal-validator" # => true
is :my_validator?, "value to validate", "hello", 42 # => true


# raises an error if the email is not valid
is! :email?, "contact@@example..org" # => Validator::Error

is is a macro, no overhead during the runtime 🚀 By the nature of the macros, you can't pass the validator name dynamically with a variable like that is(validator_name, "my value to validate", arg). But of course you can pass arguments with variables is(:validator_name?, arg1, arg2).

Validation rules

The validation rules can be defined directly when defining properties (with getter or property). Or with the macro Check.rules. Depending on preference, it's the same under the hood.

require "validator/check"

class User
  # Mixin
  Check.checkable

  # required
  property email : String, {
    required: true,

    # Optional lifecycle hook to be executed on `check_email` call.
    # Before the `check` rules, just after `clean_email` called inside `check_email`.
    # Proc or method name (here is a Proc)
    before_check: ->(v : Check::Validation, content : String?, required : Bool, format : Bool) {
      puts "before_check_content"
      content
    },

    # Optional lifecycle hook to be executed on `check_email` call, after the `check` rules.
    # Proc or method name (here is the method name)
    after_check: :after_check_email

    # Checker (all validators are supported)
    check: {
      not_empty: {"Email is required"},
      email:     {"It is not a valid email"},
    },

    # Cleaner
    clean: {
      # Data type
      type: String,

      # Converter (if union or other) to the expected value type.
      # Example if the input value is i32, but i64 is expected
      # Here is a String
      to: :to_s,

      # Formatter (any Crystal Proc) or method name (Symbol)
      format: :format_email,

      # Error message
      # Default is "Wrong type" but it can be customized
      message: "Oops! Wrong type.",
    }
  }

  # required
  property age : Int32, {
    required: "Age is required", # Custom message
    check: {
      min:     {"Age should be more than 18", 18},
      between: {"Age should be between 25 and 35", 25, 35},
    },
    clean: {type: Int32, to: :to_i32, message: "Unable to cast to Int32"},
  }

  # nilable
  property bio : String?, {
    check: {
      between: {"The user bio must be between 2 and 400 characters.", 2, 400},
    },
    clean: {
      type: String,
      to: :to_s,
      # `nilable` means omited if not provided,
      # regardless of Crystal type (nilable or not)
      nilable: true
    },
  }

  def initialize(@email, @age)
  end

  # ---------------------------------------------------------------------------
  # Lifecycle methods (hooks)
  # ---------------------------------------------------------------------------

  # Triggered on instance: `user.check`
  private def before_check(v : Check::Validation, required : Bool, format : Bool)
    # Code...
  end

  # Triggered on instance: `user.check`
  private def after_check(v : Check::Validation, required : Bool, format : Bool)
    # Code...
  end

  # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)
  private def self.before_check(v : Check::Validation, h, required : Bool, format : Bool)
    # Code...
    pp h
  end

  # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)
  private def self.after_check(v : Check::Validation, h, cleaned_h, required : Bool, format : Bool)
    # Code...
    pp cleaned_h
    cleaned_h # <= returns cleaned_h!
  end

  # Triggered on a static call and on instance call: `User.check_email(value)`, `User.check(h)`, `user.check`.
  private def self.after_check_content(v : Check::Validation, content : String?, required : Bool, format : Bool)
    puts "after_check_content"
    puts "Valid? #{v.valid?}"
    content
  end

  # --------------------------------------------------------------------------
  #  Custom checkers
  # --------------------------------------------------------------------------

  # Triggered on instance: `user.check`
  @[Check::Checker]
  private def custom_checker(v : Check::Validation, required : Bool, format : Bool)
    # Code...
  end

    # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)
  @[Check::Checker]
  private def self.custom_checker(v : Check::Validation, h, cleaned_h, required : Bool, format : Bool)
    # Code...
    cleaned_h # <= returns cleaned_h!
  end

  # --------------------------------------------------------------------------
  #  Formatters
  # --------------------------------------------------------------------------

  # Format (convert) email.
  def self.format_email(email)
    puts "mail stripped"
    email.strip
  end

  # --------------------------------------------------------------------------
  # Normal methods
  # --------------------------------------------------------------------------

  def foo()
    # Code...
  end

  def self.bar(v)
    # Code...
  end

  # ...
end

Check with this example class (User):

# Check a Hash (statically)
v, user_h = User.check(input_h)

pp v # => Validation instance
pp v.valid?
pp v.errors

pp user_h # => Casted and cleaned Hash

# Same but raise if there is a validation error
user_h = User.check!(input_h)

# Check a Hash (on instance)
user = user.new("[email protected]", 38)

v = user.check # => Validation instance
pp v.valid?
pp v.errors

# Same but raise if there is a validation error
user.check! # => Validation instance

# Example with an active record model
user.check!.save

# Check field
v, email = User.check_email(value: "[email protected]")
v, age = User.check_age(value: 42)

# Same but raise if there is a validation error
email = User.check_email!(value: "[email protected]")

v, email = User.check_email(value: "[email protected] ", format: true)
v, email = User.check_email(value: "[email protected] ", format: false)

# Using an existing Validation instance
v = Check.new_validation
v, email = User.check_email(v, value: "[email protected]")

# Same but raise if there is a validation error
email = User.check_email!(v, value: "[email protected]")

Clean with this example class (User):

# `check` method cleans all values of the Hash (or JSON::Any),
# before executing the validation rules
v, user_h = User.check(input_h)

pp v # => Validation instance
pp v.valid?
pp v.errors

pp user_h # => Casted and cleaned Hash

# Cast and clean field
ok, email = User.clean_email(value: "[email protected]")
ok, age = User.clean_age(value: 42)

ok, email = User.clean_email(value: "[email protected] ", format: true)
ok, email = User.clean_email(value: "[email protected] ", format: false)

puts "${email} is casted and cleaned" if ok
# or
puts "Email type error" unless ok
  • clean_* methods are useful to caste a union value (like Hash or JSON::Any).
  • Also clean_* methods are optional and handy for formatting values, such as the strip on the email in the example User class.

More details about cleaning, casting, formatting and return values:

By default format is true, to disable:

ok, email = User.clean_email(value: "[email protected]", format: false)
# or
ok, email = User.clean_email("[email protected]", false)

Always use named argument if there is only one (the value):

ok, email = User.clean_email(value: "[email protected]")

ok is a boolean value that reports whether the cast succeeded. Like the type assertions in Go (lang). But the ok value is returned in first (like in Elixir lang) for easy handling of multiple return values (Tuple).

Example with multiple values returned:

ok, value1, value2 = User.clean_my_tuple({1, 2, 3})

# Same but raise if there is a validation error
value1, value2 = User.clean_my_tuple!({1, 2, 3})

Considering the example class above (User). As a reminder, the email field has been defined with the formatter below:

Check.rules(
  email: {
    clean: {
      type:    String,
      to:      :to_s,
      format:  ->self.format_email(String), # <= Here!
      message: "Wrong type",
    },
  },
)

# ...

# Format (convert) email.
def self.format_email(email)
  puts "mail stripped"
  email.strip
end

So clean_email cast to String and strip the value " [email protected] ":

# Email value with one space before and one space after
ok, email = User.clean_email(value: " [email protected] ")

puts email # => "[email protected]"

# Same but raise if there is a validation error
# Email value with one space before and one space after
email = User.clean_email!(value: " [email protected] ")

puts email # => "[email protected]"

If the email was taken from a union type (json["email"]?), the returned email variable would be a String too.

See more examples.

NOTE: Require more explanations about required, nilable rules. Also about the converters JSON / Crystal Hash: h_from_json, to_json_h, to_crystal_h. In the meantime see the API doc.

Validation#check

To perform a series of validations with error handling, the validator/check module offers this possibility 👍

A Validation instance provides the means to write sequential checks, fine-tune each micro-validation with their own rules and custom error message, the possibility to retrieve all error messages, etc.

Validation is also used with Check.rules and Check.checkable that provide a powerful and productive system of validation rules which makes data cleaning and data validation in Crystal very easy. With self-generated granular methods for cleaning and checking data.

To use the checker (check) includes in the Validation class:

require "validator/check"

# Validates the *user* data received in the HTTP controller or other.
def validate_user(user : Hash) : Check::Validation
  v = Check.new_validation

  # -- email

  # Hash key can be a String or a Symbol
  v.check :email, "The email is required.", is :presence?, :email, user

  v.check "email", "The email is required.", is :presence?, "email", user
  v.check "email", "#{user["email"]} is an invalid email.", is :email?, user["email"]

  # -- username

  v.check "username", "The username is required.", is :presence?, "username", user

  v.check(
    "username",
    "The username must contain at least 2 characters.",
    is :min?, user["username"], 2
  )

  v.check(
    "username",
    "The username must contain a maximum of 20 characters.",
    is :max?, user["username"], 20
  )
end

v = validate_user user

pp v.valid? # => true (or false)

# Inverse of v.valid?
if v.errors.empty?
  return "no error"
end

# Print all the errors (if any)
pp v.errors

# It's a Hash of Array
errors = v.errors

puts errors.size
puts errors.first_value

errors.each do |key, messages|
  puts key   # => "username"
  puts messages # => ["The username is required.", "etc..."]
end

3 methods #check:

# check(key : Symbol | String, valid : Bool)
# Using default error message
v.check(
  "username",
  is(:min?, user["username"], 2)
)

# check(key : Symbol | String, message : String, valid : Bool)
# Using custom error message
v.check(
  "username",
  "The username must contain at least 2 characters.",
  is(:min?, user["username"], 2)
)

# check(key : Symbol | String, valid : Bool, message : String)
# Using custom error message
v.check(
  "username",
  is(:min?, user["username"], 2),
  "The username must contain at least 2 characters."
)

Check is a simple and lightweight wrapper. The Check::Validation is agnostic of the checked data, of the context (model, controller, CSV file, HTTP data, socket data, JSON, etc).

Use case example: Before saving to the database or process user data for a particular task, the custom error messages can be used for the end user response.

But a Validation instance can be used just to store validation errors:

v = Check.new_validation
v.add_error("foo", "foo error!")
pp v.errors # => {"foo" => ["foo error!"]}

See also Check.rules and Check.checkable.

Let your imagination run wild to add your logic around it.

Custom validator

Just add your own method to register a custom validator or to overload an existing validator.

module Validator
  # My custom validator
  def self.my_validator?(value, arg : String, another_arg : Int32) : Bool
    # write here the logic of your validator...
    return true
  end
end

# Call it
puts Valid.my_validator?("value to validate", "hello", 42) # => true

# or with the `is` flavor
puts is :my_validator?, "value to validate", "hello", 42 # => true

Using the custom validator with the validation rules:

require "validator/check"

class Article
  # Mixin
  Check.checkable

  property title : String
  property content : String

  Check.rules(
    content: {
      # Now the custom validator is available
      check: {
        my_validator: {"My validator error message"},
        between: {"The article content must be between 10 and 20 000 characters", 10, 20_000},
        # ...
      },
    },
  )
end

# Triggered with all data
v, article = Article.check(input_data)

# Triggered with one value
v, content = Article.check_content(input_data["content"]?)

Conventions

  • The word "validator" is the method to make a "validation" (value validation).
  • A validator returns true if the value (or/and the condition) is valid, false if not.
  • The first argument(s) is (are) the value(s) to be validated.
  • Always add the Bool return type to a validator.
  • Always add the suffix ? to the method name of a validator.
  • If possible, indicates the type of the validator arguments.
  • Spec: Battle tested.
  • KISS and Unix Philosophy.

Development

crystal spec
crystal tool format
./bin/ameba

Contributing

  1. Fork it (https://github.com/nicolab/crystal-validator/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

LICENSE

MIT (c) 2020, Nicolas Talle.

Author

Nicolas Tallefourtane - Nicolab.net
Nicolas Talle
Make a donation via Paypal

Thanks to ilourt for his great work on checkable mixins (clean_, check_, ...).

More Repositories

1

php-ftp-client

A flexible FTP and SSL-FTP client for PHP
PHP
632
star
2

atom-local-history

Atom package for maintaining local history of files.
JavaScript
77
star
3

evemit

Minimal and fast JavaScript event emitter for Node.js and front-end (only 1kb)
JavaScript
15
star
4

crystal-dbx

ORM and query builder for Crystal lang.
Crystal
15
star
5

crystal-result

💎 Rust-like error handling for Crystal (`Ok` / `Err`)
Crystal
9
star
6

atom-package-js-generator

[No longer maintained] Generate Atom.io packages in Javascript instead of CoffeeScript.
JavaScript
8
star
7

crystal-crypt

💎 Cryptographic utilities made easy for Crystal lang.
Crystal
7
star
8

storux

Easy and powerful state store manager.
JavaScript
7
star
9

crystal-lru-cache

💎 key/value LRU cache that supports lifecycle, global size limit and expiration time.
Crystal
7
star
10

node-ipc-events

Inter process (IPC) event emitter
JavaScript
6
star
11

gulp-if-else

[Gulp plugin] Conditional task with "if" callback and "else" callback (optional) : gulp.src(source).pipe( ifElse(condition, ifCallback, elseCallback) )
JavaScript
6
star
12

eslint-config-common

An ESLint Shareable Config for Common JavaScript Coding style.
JavaScript
5
star
13

crystal-prop

Properties utilities for Crystal lang (improved getter, IoC, factory, ...).
Crystal
5
star
14

routux

A fast and productive router to improve the UX (User eXperience) of any front-end application (supports middlewares, regex pattern, named routes, error handler, ...).
JavaScript
5
star
15

mongoose-tags

A simple tagging plugin for Mongoose
JavaScript
4
star
16

granite-paginate

Crystal shard adding pagination support for Granite ORM.
Crystal
3
star
17

envlist

envlist is a micro-module (without dependency) for resolving the type of runtime environment between your application convention and another convention (like NODE_ENV).
JavaScript
3
star
18

waitwait

Golang's `WaitGroup` and Unix's `sleep` for Javascript (browser and Node.js).
JavaScript
2
star
19

goflow

A base workflow for Golang projects: Docker + Golang + inspiration from standards.
Shell
2
star
20

crystal-testify

Testing utilities for Crystal lang specs. OOP abstraction for creating unit and integration tests.
Crystal
2
star
21

qfiles.js

Helpers for handling files with Node.js, without dependencies (requireAll, requireToObj, RequireFiles, ...).
JavaScript
2
star
22

Nicolab

1
star
23

node-spawn-handler

Simple handler for ChildProcess.spawn of Node.js
JavaScript
1
star
24

eslint-config-common-jsx

An ESLint Shareable Config for JSX support in JavaScript Common coding Style
JavaScript
1
star
25

node-error-render

[Node.js] Prettify the details of the errors in the console
JavaScript
1
star
26

eslint-config-common-react

An ESLint Shareable Config for React/JSX support in JavaScript Common coding Style
JavaScript
1
star
27

core-stack

The base of a Javascript core object with builtin: plugin system, event emitter and stack handler.
JavaScript
1
star
28

atom-helpers

A Node.JS package that provides helpers for Atom.io packages development
JavaScript
1
star
29

rust-guess-game

ʘ‿ʘ ▲ A little guessing game developed in Rust for fun and discovery ▼ ʘ‿ʘ
Rust
1
star
30

node-spawn-rmrf

Removes recursively with rm -rf `./file/path` (spawned).
JavaScript
1
star