├── .gitignore ├── .editorconfig ├── shard.yml ├── spec ├── assertions │ ├── is_nil_spec.cr │ ├── not_nil_spec.cr │ ├── is_true_spec.cr │ ├── is_false_spec.cr │ ├── is_blank_spec.cr │ ├── not_blank_spec.cr │ ├── email_validation_mode_spec.cr │ ├── divisible_by_spec.cr │ ├── valid_spec.cr │ ├── equal_to_spec.cr │ ├── not_equal_to_spec.cr │ ├── ip_version_spec.cr │ ├── regex_match_spec.cr │ ├── less_than_spec.cr │ ├── greater_than_spec.cr │ ├── less_than_or_equal_spec.cr │ ├── greater_than_or_equal_spec.cr │ ├── email_spec.cr │ ├── ip_spec.cr │ ├── in_range_spec.cr │ ├── choice_spec.cr │ ├── url_spec.cr │ ├── uuid_spec.cr │ └── size_spec.cr ├── spec_helper.cr ├── assertion_spec.cr ├── exceptions │ └── validation_error_spec.cr └── assert_spec.cr ├── .github └── workflows │ ├── deployment.yml │ └── ci.yml ├── src ├── assertions │ ├── is_nil.cr │ ├── not_nil.cr │ ├── is_true.cr │ ├── is_false.cr │ ├── divisible_by.cr │ ├── is_blank.cr │ ├── valid.cr │ ├── not_blank.cr │ ├── equal_to.cr │ ├── not_equal_to.cr │ ├── regex_match.cr │ ├── less_than.cr │ ├── greater_than.cr │ ├── less_than_or_equal.cr │ ├── greater_than_or_equal.cr │ ├── email.cr │ ├── uuid.cr │ ├── in_range.cr │ ├── choice.cr │ ├── ip.cr │ ├── size.cr │ └── url.cr ├── exceptions │ └── validation_error.cr ├── assert.cr └── assertion.cr ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in applications that use them 9 | /shard.lock 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: assert 2 | 3 | description: | 4 | Extensible annotation based object validation library 5 | 6 | version: 0.2.1 7 | 8 | authors: 9 | - Blacksmoke16 10 | 11 | crystal: 0.30.0 12 | 13 | license: MIT 14 | 15 | development_dependencies: 16 | ameba: 17 | github: crystal-ameba/ameba 18 | version: ~> 0.13.0 19 | -------------------------------------------------------------------------------- /spec/assertions/is_nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::IsNil do 4 | assert_template(Assert::Assertions::IsNil, "'%{property_name}' should be null") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::IsNil) 8 | 9 | describe "with a non nil value" do 10 | it "should be invalid" do 11 | Assert::Assertions::IsNil(Bool?).new("prop", false).valid?.should be_false 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/assertions/not_nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::NotNil do 4 | assert_template(Assert::Assertions::NotNil, "'%{property_name}' should not be null") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::NotNil, valid: false) 8 | 9 | describe "with a non nil value" do 10 | it "should be valid" do 11 | Assert::Assertions::NotNil(Bool?).new("prop", false).valid?.should be_true 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy_docs: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: crystallang/crystal 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Build 16 | run: crystal docs 17 | - name: Deploy 18 | uses: JamesIves/github-pages-deploy-action@2.0.3 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | BRANCH: gh-pages 22 | FOLDER: docs 23 | -------------------------------------------------------------------------------- /spec/assertions/is_true_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::IsTrue do 4 | assert_template(Assert::Assertions::IsTrue, "'%{property_name}' should be true") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::IsTrue, [Bool?]) 8 | 9 | describe "with true" do 10 | it "should be valid" do 11 | Assert::Assertions::IsTrue(Bool).new("prop", true).valid?.should be_true 12 | end 13 | end 14 | 15 | describe "with false" do 16 | it "should be invalid" do 17 | Assert::Assertions::IsTrue(Bool?).new("prop", false).valid?.should be_false 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/assertions/is_false_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::IsFalse do 4 | assert_template(Assert::Assertions::IsFalse, "'%{property_name}' should be false") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::IsFalse, [Bool?]) 8 | 9 | describe "with true" do 10 | it "should be invalid" do 11 | Assert::Assertions::IsFalse(Bool).new("prop", true).valid?.should be_false 12 | end 13 | end 14 | 15 | describe "with false" do 16 | it "should be valid" do 17 | Assert::Assertions::IsFalse(Bool?).new("prop", false).valid?.should be_true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/assertions/is_nil.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::IsNil)] 4 | # Validates a property is `nil`. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::IsNil] 14 | # property nilable_int : Int32? = nil 15 | # 16 | # @[Assert::IsNil] 17 | # property nilable_string : String? = nil 18 | # end 19 | # 20 | # Example.new.valid? # => true 21 | # ``` 22 | class Assert::Assertions::IsNil(PropertyType) < Assert::Assertions::Assertion 23 | initializer( 24 | actual: PropertyType 25 | ) 26 | 27 | # :inherit: 28 | def default_message_template : String 29 | "'%{property_name}' should be null" 30 | end 31 | 32 | # :inherit: 33 | def valid? : Bool 34 | @actual.nil? 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/assertions/not_nil.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::NotNil)] 4 | # Validates a property is not `nil`. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::NotNil] 14 | # property nilable_int : Int32? = 17 15 | # 16 | # @[Assert::NotNil] 17 | # property nilable_string : String? = "Bob" 18 | # end 19 | # 20 | # Example.new.valid? # => true 21 | # ``` 22 | class Assert::Assertions::NotNil(PropertyType) < Assert::Assertions::Assertion 23 | initializer( 24 | actual: PropertyType 25 | ) 26 | 27 | # :inherit: 28 | def default_message_template : String 29 | "'%{property_name}' should not be null" 30 | end 31 | 32 | # :inherit: 33 | def valid? : Bool 34 | !@actual.nil? 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/assertions/is_true.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::IsTrue)] 4 | # Validates a property is `true`. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::IsTrue] 14 | # property bool : Bool = true 15 | # 16 | # @[Assert::IsTrue] 17 | # property nilable_bool : Bool? = nil 18 | # end 19 | # 20 | # Example.new.valid? # => true 21 | # ``` 22 | class Assert::Assertions::IsTrue(PropertyType) < Assert::Assertions::Assertion 23 | initializer( 24 | actual: Bool? 25 | ) 26 | 27 | # :inherit: 28 | def default_message_template : String 29 | "'%{property_name}' should be true" 30 | end 31 | 32 | # :inherit: 33 | def valid? : Bool 34 | return true if @actual.nil? 35 | @actual == true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/assertions/is_false.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::IsFalse)] 4 | # Validates a property is `false`. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::IsFalse] 14 | # property bool : Bool = false 15 | # 16 | # @[Assert::IsFalse] 17 | # property nilable_bool : Bool? = nil 18 | # end 19 | # 20 | # Example.new.valid? # => true 21 | # ``` 22 | class Assert::Assertions::IsFalse(PropertyType) < Assert::Assertions::Assertion 23 | initializer( 24 | actual: Bool? 25 | ) 26 | 27 | # :inherit: 28 | def default_message_template : String 29 | "'%{property_name}' should be false" 30 | end 31 | 32 | # :inherit: 33 | def valid? : Bool 34 | return true if @actual.nil? 35 | @actual == false 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/assert" 3 | 4 | # Define a nil value test case for *type* should be *valid*. 5 | macro assert_nil(assertion, type_vars = [String?], valid = true, **args) 6 | describe "with a nil value" do 7 | it %(should #{{{valid ? "" : "not"}}} be valid) do 8 | {{assertion.id}}({{type_vars.splat}}).new("prop", nil, {{args.double_splat}}).valid?.should be_{{valid.id}} 9 | end 10 | end 11 | end 12 | 13 | # Defines a test case for testing *type*'s default_message_template equals *message*. 14 | macro assert_template(type, message, type_vars = [String?], **args) 15 | describe "#default_message_template" do 16 | it "should return the proper default template string" do 17 | {{type.id}}({{type_vars.splat}}).new("prop", nil, {{args.double_splat}}).default_message_template.should eq {{message}} 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/assertions/is_blank_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::IsBlank do 4 | assert_template(Assert::Assertions::IsBlank, "'%{property_name}' should be blank") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::IsBlank) 8 | 9 | describe "with a non blank value" do 10 | it "should be invalid" do 11 | Assert::Assertions::IsBlank(String?).new("prop", "Jim").valid?.should be_false 12 | end 13 | end 14 | 15 | describe "with a blank value" do 16 | it "should be valid" do 17 | Assert::Assertions::IsBlank(String?).new("prop", "").valid?.should be_true 18 | end 19 | end 20 | 21 | describe :normalizer do 22 | it "should alter the value before checking its validity" do 23 | Assert::Assertions::IsBlank(String?).new("prop", " m", normalizer: ->(actual : String) { actual.chomp('m') }).valid?.should be_true 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/assertions/not_blank_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::NotBlank do 4 | assert_template(Assert::Assertions::NotBlank, "'%{property_name}' should not be blank") 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::NotBlank) 8 | 9 | describe "with a non blank value" do 10 | it "should be valid" do 11 | Assert::Assertions::NotBlank(String?).new("prop", "Jim").valid?.should be_true 12 | end 13 | end 14 | 15 | describe "with a blank value" do 16 | it "should be invalid" do 17 | Assert::Assertions::NotBlank(String?).new("prop", "").valid?.should be_false 18 | end 19 | end 20 | 21 | describe :normalizer do 22 | it "should alter the value before checking its validity" do 23 | Assert::Assertions::NotBlank(String?).new("prop", "", normalizer: ->(actual : String) { actual + 'f' }).valid?.should be_true 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Blacksmoke16 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/assertions/email_validation_mode_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::Assertions::Email::EmailValidationMode do 4 | describe "#get_pattern" do 5 | describe Assert::Assertions::Email::EmailValidationMode::Loose do 6 | it "should return the correct Regex" do 7 | Assert::Assertions::Email::EmailValidationMode::Loose.get_pattern.should eq /^.+\@\S+\.\S+$/ 8 | end 9 | end 10 | 11 | describe Assert::Assertions::Email::EmailValidationMode::HTML5 do 12 | it "should return the correct Regex" do 13 | Assert::Assertions::Email::EmailValidationMode::HTML5.get_pattern.should eq /^[a-zA-Z0-9.!\#$\%&\'*+\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/ 14 | end 15 | end 16 | 17 | describe Assert::Assertions::Email::EmailValidationMode::Strict do 18 | it "should raise an NotImplementedError" do 19 | expect_raises(NotImplementedError, "Unsupported pattern: Strict") do 20 | Assert::Assertions::Email::EmailValidationMode::Strict.get_pattern 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated in favor of Athena's [Validator](https://github.com/athena-framework/validator) component. 2 | 3 | # Assert 4 | [![Latest release](https://img.shields.io/github/release/Blacksmoke16/assert.svg?style=flat-square)](https://github.com/Blacksmoke16/assert/releases) 5 | 6 | Extensible annotation based object validation library based on [Symfony Validation Constraint Annotations](https://symfony.com/doc/current/reference/constraints.html). 7 | 8 | ## Installation 9 | 10 | Add this to your application's `shard.yml`: 11 | 12 | ```yaml 13 | dependencies: 14 | assert: 15 | github: blacksmoke16/assert 16 | ``` 17 | 18 | ## Documentation 19 | 20 | Everything is documented in the [API Docs](https://blacksmoke16.github.io/assert/Assert.html). 21 | 22 | ## Contributing 23 | 24 | 1. Fork it () 25 | 2. Create your feature branch (`git checkout -b my-new-feature`) 26 | 3. Commit your changes (`git commit -am 'Add some feature'`) 27 | 4. Push to the branch (`git push origin my-new-feature`) 28 | 5. Create a new Pull Request 29 | 30 | ## Contributors 31 | 32 | - [Blacksmoke16](https://github.com/Blacksmoke16) Blacksmoke16 - creator, maintainer 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*/*' 7 | - '*' 8 | - '!master' 9 | 10 | jobs: 11 | check_format: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: crystallang/crystal:latest-alpine 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Format 18 | run: crystal tool format --check 19 | coding_standards: 20 | runs-on: ubuntu-latest 21 | container: 22 | image: crystallang/crystal:latest-alpine 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Install Dependencies 26 | run: shards install 27 | - name: Ameba 28 | run: ./bin/ameba 29 | test_latest: 30 | runs-on: ubuntu-latest 31 | container: 32 | image: crystallang/crystal:latest-alpine 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Specs 36 | run: crystal spec --order random --error-on-warnings 37 | test_nightly: 38 | runs-on: ubuntu-latest 39 | container: 40 | image: crystallang/crystal:nightly-alpine 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Specs 44 | run: crystal spec --order random --error-on-warnings 45 | -------------------------------------------------------------------------------- /spec/assertions/divisible_by_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::DivisibleBy do 4 | assert_template(Assert::Assertions::DivisibleBy, "'%{property_name}' should be a multiple of '%{value}'", value: 19.0, type_vars: [Float64?]) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::DivisibleBy, value: 10, type_vars: [Int32?]) 8 | 9 | describe "with divisible values" do 10 | describe Int do 11 | it "should be valid" do 12 | Assert::Assertions::DivisibleBy(Int32).new("prop", 150, 25).valid?.should be_true 13 | end 14 | end 15 | 16 | describe Float64 do 17 | it "should be valid" do 18 | Assert::Assertions::DivisibleBy(Float64?).new("prop", 15.75, 0.25).valid?.should be_true 19 | end 20 | end 21 | end 22 | 23 | describe "with undivisible values" do 24 | describe Int do 25 | it "should be invalid" do 26 | Assert::Assertions::DivisibleBy(Int32).new("prop", 150, 19).valid?.should be_false 27 | end 28 | end 29 | 30 | describe Float64 do 31 | it "should be invalid" do 32 | Assert::Assertions::DivisibleBy(Float64?).new("prop", 15.75, 0.3).valid?.should be_false 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/assertions/divisible_by.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::DivisibleBy)] 4 | # Validates a property is divisible by *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::DivisibleBy(value: 0.5)] 14 | # property float : Float64 = 100.0 15 | # 16 | # @[Assert::DivisibleBy(value: 2)] 17 | # property int : Int32 = 1234 18 | # 19 | # @[Assert::DivisibleBy(value: get_value)] 20 | # property getter_property : UInt16 = 256_u16 21 | # 22 | # def get_value : UInt16 23 | # 16_u16 24 | # end 25 | # end 26 | # 27 | # Example.new.valid? # => true 28 | # ``` 29 | # 30 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 31 | # NOTE: The type of *value* and the property must match. 32 | class Assert::Assertions::DivisibleBy(PropertyType) < Assert::Assertions::Assertion 33 | initializer( 34 | actual: PropertyType, 35 | value: PropertyType 36 | ) 37 | 38 | # :inherit: 39 | def default_message_template : String 40 | "'%{property_name}' should be a multiple of '%{value}'" 41 | end 42 | 43 | # :inherit: 44 | def valid? : Bool 45 | return true unless (actual = @actual) && (value = @value) 46 | (actual % value).zero? 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/assertions/is_blank.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::IsBlank)] 4 | # Validates a property is blank, that is, consists exclusively of unicode whitespace. 5 | # 6 | # ### Optional Arguments 7 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 8 | # 9 | # ### Example 10 | # ``` 11 | # class Example 12 | # include Assert 13 | # 14 | # def initialize; end 15 | # 16 | # @[Assert::IsBlank] 17 | # property string : String = "" 18 | # 19 | # @[Assert::IsBlank] 20 | # property multiple_spaces : String = " " 21 | # 22 | # @[Assert::IsBlank] 23 | # property nilble_string : String? = nil 24 | # 25 | # @[Assert::IsBlank(normalizer: ->(actual : String) { actual.chomp('a') })] 26 | # property normalizer : String = " a" 27 | # end 28 | # 29 | # Example.new.valid? # => true 30 | # ``` 31 | class Assert::Assertions::IsBlank(PropertyType) < Assert::Assertions::Assertion 32 | initializer( 33 | actual: String?, 34 | normalizer: "Proc(String, String)? = nil" 35 | ) 36 | 37 | # :inherit: 38 | def default_message_template : String 39 | "'%{property_name}' should be blank" 40 | end 41 | 42 | # :inherit: 43 | def valid? : Bool 44 | return true unless actual = @actual 45 | ((normalizer = @normalizer) ? normalizer.call actual : actual).blank? 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/assertions/valid.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Valid)] 4 | # Validates child object(s) are valid; rendering the parent object invalid if any assertions on the child object(s) fail. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Foo 9 | # include Assert 10 | # 11 | # def initialize(@some_val : Int32); end 12 | # 13 | # @[Assert::EqualTo(value: 50)] 14 | # property some_val : Int32 15 | # end 16 | # 17 | # class Example 18 | # include Assert 19 | # 20 | # def initialize; end 21 | # 22 | # @[Assert::EqualTo(value: 100)] 23 | # property int32 : Int32 = 100 24 | # 25 | # @[Assert::Valid] 26 | # property foo : Foo = Foo.new(50) 27 | # 28 | # @[Assert::Valid] 29 | # property foos : Array(Foo) = [Foo.new(50), Foo.new(50)] 30 | # end 31 | # 32 | # Example.new.valid? # => true 33 | # ``` 34 | class Assert::Assertions::Valid(PropertyType) < Assert::Assertions::Assertion 35 | initializer( 36 | actual: PropertyType 37 | ) 38 | 39 | # :inherit: 40 | def default_message_template : String 41 | "'%{property_name}' should be valid" 42 | end 43 | 44 | # :inherit: 45 | def valid? : Bool 46 | return true unless actual = @actual 47 | case actual 48 | when Array then actual.all?(&.valid?) 49 | when Assert then actual.valid? 50 | else 51 | true 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/assertions/not_blank.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::NotBlank)] 4 | # Validates a property is not blank, that is, does not consist exclusively of unicode whitespace. 5 | # 6 | # ### Optional Arguments 7 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 8 | # 9 | # ### Example 10 | # ``` 11 | # class Example 12 | # include Assert 13 | # 14 | # def initialize; end 15 | # 16 | # @[Assert::NotBlank] 17 | # property string : String = "foo" 18 | # 19 | # @[Assert::NotBlank] 20 | # property single_character : String = " a " 21 | # 22 | # @[Assert::NotBlank] 23 | # property nilble_string : String? = nil 24 | # 25 | # @[Assert::NotBlank(normalizer: ->(actual : String) { "Hello #{actual}" })] 26 | # property normalizer : String = "" 27 | # end 28 | # 29 | # Example.new.valid? # => true 30 | # ``` 31 | class Assert::Assertions::NotBlank(PropertyType) < Assert::Assertions::Assertion 32 | initializer( 33 | actual: String?, 34 | normalizer: "Proc(String, String)? = nil" 35 | ) 36 | 37 | # :inherit: 38 | def default_message_template : String 39 | "'%{property_name}' should not be blank" 40 | end 41 | 42 | # :inherit: 43 | def valid? : Bool 44 | return true unless actual = @actual 45 | !((normalizer = @normalizer) ? normalizer.call actual : actual).blank? 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/assertions/equal_to.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::EqualTo)] 4 | # Validates a property is equal to *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::EqualTo(value: 100)] 14 | # property int32 : Int32 = 100 15 | # 16 | # @[Assert::EqualTo(value: 0.0001)] 17 | # property float : Float64 = 0.0001 18 | # 19 | # @[Assert::EqualTo(value: "X")] 20 | # property string : String = "X" 21 | # 22 | # @[Assert::EqualTo(value: max_value)] 23 | # property getter_property : UInt8 = 255_u8 24 | # 25 | # def max_value : UInt8 26 | # 255_u8 27 | # end 28 | # end 29 | # 30 | # Example.new.valid? # => true 31 | # ``` 32 | # 33 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 34 | # NOTE: The type of *value* and *actual* must match. 35 | # NOTE: `PropertyType` can be anything that defines a `#==` method. 36 | class Assert::Assertions::EqualTo(PropertyType) < Assert::Assertions::Assertion 37 | initializer( 38 | actual: PropertyType, 39 | value: PropertyType 40 | ) 41 | 42 | # :inherit: 43 | def default_message_template : String 44 | "'%{property_name}' should be equal to '%{value}'" 45 | end 46 | 47 | # :inherit: 48 | def valid? : Bool 49 | @actual == @value 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/assertions/not_equal_to.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::NotEqualTo)] 4 | # Validates a property is not equal to *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::NotEqualTo(value: 100)] 14 | # property int32 : Int32 = 50 15 | # 16 | # @[Assert::NotEqualTo(value: 0.0001)] 17 | # property float : Float64 = 0.00001 18 | # 19 | # @[Assert::NotEqualTo(value: "X")] 20 | # property string : String = "Y" 21 | # 22 | # @[Assert::NotEqualTo(value: max_value)] 23 | # property getter_property : UInt8 = 255_u8 24 | # 25 | # def max_value : UInt8 26 | # 250_u8 27 | # end 28 | # end 29 | # 30 | # Example.new.valid? # => true 31 | # ``` 32 | # 33 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 34 | # NOTE: The type of *value* and *actual* must match. 35 | # NOTE: `PropertyType` can be anything that defines a `#!=` method. 36 | class Assert::Assertions::NotEqualTo(PropertyType) < Assert::Assertions::Assertion 37 | initializer( 38 | actual: PropertyType, 39 | value: PropertyType 40 | ) 41 | 42 | # :inherit: 43 | def default_message_template : String 44 | "'%{property_name}' should not be equal to '%{value}'" 45 | end 46 | 47 | # :inherit: 48 | def valid? : Bool 49 | @actual != @value 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/assertions/regex_match.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::RegexMatch)] 4 | # Validates a property matches a `Regex` pattern. 5 | # 6 | # ### Optional Arguments 7 | # * *match* - Whether the string should have to match the pattern to be valid. 8 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 9 | # 10 | # ### Example 11 | # ``` 12 | # class Example 13 | # include Assert 14 | # 15 | # def initialize; end 16 | # 17 | # @[Assert::RegexMatch(pattern: /foo==bar/)] 18 | # property match : String? = "foo==bar" 19 | # 20 | # @[Assert::RegexMatch(pattern: /foo==bar/, match: false)] 21 | # property not_match : String = "foo--bar" 22 | # 23 | # @[Assert::RegexMatch(pattern: /^foo/, normalizer: ->(actual : String) { actual.strip })] 24 | # property normalizer : String = " foo" 25 | # end 26 | # 27 | # Example.new.valid? # => true 28 | # ``` 29 | class Assert::Assertions::RegexMatch(PropertyType) < Assert::Assertions::Assertion 30 | initializer( 31 | actual: String?, 32 | pattern: Regex, 33 | match: "Bool = true", 34 | normalizer: "Proc(String, String)? = nil" 35 | ) 36 | 37 | # :inherit: 38 | def default_message_template : String 39 | "'%{property_name}' is not valid" 40 | end 41 | 42 | # :inherit: 43 | def valid? : Bool 44 | return true unless actual = @actual 45 | matched : Int32? = (((normalizer = @normalizer) ? normalizer.call actual : actual) =~ @pattern) 46 | @match ? !matched.nil? : matched.nil? 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/assertions/valid_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | class SomeObj 4 | include Assert 5 | 6 | def initialize(@some_val : Int32); end 7 | 8 | @[Assert::EqualTo(value: 50)] 9 | property some_val : Int32 10 | end 11 | 12 | describe Assert::Valid do 13 | assert_template(Assert::Assertions::Valid, "'%{property_name}' should be valid") 14 | 15 | describe "#valid?" do 16 | assert_nil(Assert::Assertions::Valid) 17 | 18 | describe Object do 19 | describe "with a valid sub object" do 20 | it "should be valid" do 21 | Assert::Assertions::Valid(SomeObj).new("prop", SomeObj.new(50)).valid?.should be_true 22 | end 23 | end 24 | 25 | describe "with an invalid object" do 26 | it "should be invalid" do 27 | Assert::Assertions::Valid(SomeObj).new("prop", SomeObj.new(100)).valid?.should be_false 28 | Assert::Assertions::Valid(SomeObj).new("prop", SomeObj.new(100)).valid?.should be_false 29 | end 30 | end 31 | end 32 | 33 | describe Array do 34 | describe "with all valid objects" do 35 | it "should be valid" do 36 | Assert::Assertions::Valid(Array(SomeObj)).new("prop", [SomeObj.new(50), SomeObj.new(50), SomeObj.new(50)]).valid?.should be_true 37 | end 38 | end 39 | 40 | describe "with at least one invalid object" do 41 | it "should be invalid" do 42 | Assert::Assertions::Valid(Array(SomeObj)).new("prop", [SomeObj.new(50), SomeObj.new(99), SomeObj.new(50)]).valid?.should be_false 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/assertions/equal_to_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::EqualTo do 4 | assert_template(Assert::Assertions::EqualTo, "'%{property_name}' should be equal to '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | describe "with equal values" do 8 | describe Bool do 9 | it "should be valid" do 10 | Assert::Assertions::EqualTo(Bool).new("prop", true, true).valid?.should be_true 11 | end 12 | end 13 | 14 | describe Array do 15 | it "should be valid" do 16 | Assert::Assertions::EqualTo(Array(Int32)).new("prop", [1, 2], [1, 2]).valid?.should be_true 17 | end 18 | end 19 | 20 | describe Time do 21 | it "should be valid" do 22 | Assert::Assertions::EqualTo(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 1)).valid?.should be_true 23 | end 24 | end 25 | end 26 | 27 | describe "with not equal values" do 28 | describe Bool do 29 | it "should be invalid" do 30 | Assert::Assertions::EqualTo(Bool).new("prop", true, false).valid?.should be_false 31 | end 32 | end 33 | 34 | describe Array do 35 | it "should be invalid" do 36 | Assert::Assertions::EqualTo(Array(Int32)).new("prop", [1, 2], [1]).valid?.should be_false 37 | end 38 | end 39 | 40 | describe Time do 41 | it "should be invalid" do 42 | Assert::Assertions::EqualTo(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 2)).valid?.should be_false 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/assertions/not_equal_to_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::NotEqualTo do 4 | assert_template(Assert::Assertions::NotEqualTo, "'%{property_name}' should not be equal to '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | describe "with equal values" do 8 | describe Bool do 9 | it "should be invalid" do 10 | Assert::Assertions::NotEqualTo(Bool).new("prop", true, true).valid?.should be_false 11 | end 12 | end 13 | 14 | describe Array do 15 | it "should be invalid" do 16 | Assert::Assertions::NotEqualTo(Array(Int32)).new("prop", [1, 2], [1, 2]).valid?.should be_false 17 | end 18 | end 19 | 20 | describe Time do 21 | it "should be invalid" do 22 | Assert::Assertions::NotEqualTo(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 1)).valid?.should be_false 23 | end 24 | end 25 | end 26 | 27 | describe "with not equal values" do 28 | describe Bool do 29 | it "should be valid" do 30 | Assert::Assertions::NotEqualTo(Bool).new("prop", true, false).valid?.should be_true 31 | end 32 | end 33 | 34 | describe Array do 35 | it "should be valid" do 36 | Assert::Assertions::NotEqualTo(Array(Int32)).new("prop", [1, 2], [1]).valid?.should be_true 37 | end 38 | end 39 | 40 | describe Time do 41 | it "should be valid" do 42 | Assert::Assertions::NotEqualTo(Time).new("prop", (Time.utc.at_beginning_of_second + 1.hour), Time.utc.at_beginning_of_second).valid?.should be_true 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/assertions/less_than.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::LessThan)] 4 | # Validates a property is less than *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::LessThan(value: 100)] 14 | # property int32 : Int32 = 95 15 | # 16 | # @[Assert::LessThan(value: 0.0001_f64)] 17 | # property float : Float64 = 0.000001 18 | # 19 | # @[Assert::LessThan(value: "X")] 20 | # property string : String = "F" 21 | # 22 | # @[Assert::LessThan(value: Time.utc(2019, 6, 1))] 23 | # property start_date : Time? = Time.utc(2019, 5, 29) 24 | # 25 | # @[Assert::LessThan(value: start_date)] 26 | # property end_date : Time? 27 | # 28 | # @[Assert::LessThan(value: max_value)] 29 | # property getter_property : UInt8 = 250_u8 30 | # 31 | # def max_value : UInt8 32 | # 255_u8 33 | # end 34 | # end 35 | # 36 | # Example.new.valid? # => true 37 | # ``` 38 | # 39 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 40 | # NOTE: The type of *value* and the property must match. 41 | # NOTE: `PropertyType` can be anything that defines a `#<` method. 42 | class Assert::Assertions::LessThan(PropertyType) < Assert::Assertions::Assertion 43 | initializer( 44 | actual: PropertyType, 45 | value: PropertyType 46 | ) 47 | 48 | # :inherit: 49 | def default_message_template : String 50 | "'%{property_name}' should be less than '%{value}'" 51 | end 52 | 53 | # :inherit: 54 | def valid? : Bool 55 | (value = @value) && (actual = @actual) ? actual < value : true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/assertions/ip_version_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::Assertions::Ip::IPVersion do 4 | describe "#get_pattern" do 5 | describe Assert::Assertions::Ip::IPVersion::IPV4 do 6 | it "should return the correct Regex" do 7 | Assert::Assertions::Ip::IPVersion::IPV4.get_pattern.should eq /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ 8 | end 9 | end 10 | 11 | describe Assert::Assertions::Ip::IPVersion::IPV6 do 12 | it "should return the correct Regex" do 13 | Assert::Assertions::Ip::IPVersion::IPV6.get_pattern.should eq /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/assertions/regex_match_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::RegexMatch do 4 | assert_template(Assert::Assertions::RegexMatch, "'%{property_name}' is not valid", pattern: /foo/) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::RegexMatch, pattern: /foo/) 8 | 9 | describe :mode do 10 | describe :match do 11 | describe "with a matching value" do 12 | it "should be valid" do 13 | Assert::Assertions::RegexMatch(String?).new("prop", "foo", pattern: /foo/).valid?.should be_true 14 | end 15 | end 16 | 17 | describe "with a not matching value" do 18 | it "should be invalid" do 19 | Assert::Assertions::RegexMatch(String?).new("prop", "bar", pattern: /foo/).valid?.should be_false 20 | end 21 | end 22 | end 23 | 24 | describe :not_match do 25 | describe "with a matching value" do 26 | it "should be invalid" do 27 | Assert::Assertions::RegexMatch(String?).new("prop", "foo", pattern: /foo/, match: false).valid?.should be_false 28 | end 29 | end 30 | 31 | describe "with a not matching value" do 32 | it "should be valid" do 33 | Assert::Assertions::RegexMatch(String?).new("prop", "bar", pattern: /foo/, match: false).valid?.should be_true 34 | end 35 | end 36 | end 37 | end 38 | 39 | describe :normalizer do 40 | it "should alter the value before checking its validity" do 41 | Assert::Assertions::RegexMatch(String?).new("prop", " foo", pattern: /^foo/, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/assertions/greater_than.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::GreaterThan)] 4 | # Validates a property is greater than *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::GreaterThan(value: 100)] 14 | # property int32 : Int32 = 101 15 | # 16 | # @[Assert::GreaterThan(value: 0.0001_f64)] 17 | # property float : Float64 = 0.0002 18 | # 19 | # @[Assert::GreaterThan(value: "X")] 20 | # property string : String = "Y" 21 | # 22 | # @[Assert::GreaterThan(value: end_date)] 23 | # property start_date : Time? = Time.utc(2019, 5, 29) 24 | # 25 | # @[Assert::GreaterThan(value: Time.utc(2019, 1, 1))] 26 | # property end_date : Time? = Time.utc(2019, 5, 20) 27 | # 28 | # @[Assert::GreaterThan(value: max_value)] 29 | # property getter_property : UInt8 = 250_u8 30 | # 31 | # def max_value : UInt8 32 | # 200_u8 33 | # end 34 | # end 35 | # 36 | # Example.new.valid? # => true 37 | # ``` 38 | # 39 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 40 | # NOTE: The type of *value* and the property must match. 41 | # NOTE: `PropertyType` can be anything that defines a `#>` method. 42 | class Assert::Assertions::GreaterThan(PropertyType) < Assert::Assertions::Assertion 43 | initializer( 44 | actual: PropertyType, 45 | value: PropertyType 46 | ) 47 | 48 | # :inherit: 49 | def default_message_template : String 50 | "'%{property_name}' should be greater than '%{value}'" 51 | end 52 | 53 | # :inherit: 54 | def valid? : Bool 55 | (value = @value) && (actual = @actual) ? actual > value : true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/assertions/less_than_or_equal.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::LessThanOrEqual)] 4 | # Validates a property is less than or equal to *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::LessThanOrEqual(value: 100)] 14 | # property int32_property : Int32 = 100 15 | # 16 | # @[Assert::LessThanOrEqual(value: 0.0001_f64)] 17 | # property float_property : Float64 = 0.000001 18 | # 19 | # @[Assert::LessThanOrEqual(value: "X")] 20 | # property string_property : String = "X" 21 | # 22 | # @[Assert::LessThanOrEqual(value: Time.utc(2019, 6, 1))] 23 | # property start_date : Time? = Time.utc(2019, 5, 29) 24 | # 25 | # @[Assert::LessThanOrEqual(value: start_date)] 26 | # property end_date : Time? 27 | # 28 | # @[Assert::LessThanOrEqual(value: max_value)] 29 | # property getter_property : UInt8 = 250_u8 30 | # 31 | # def max_value : UInt8 32 | # 255_u8 33 | # end 34 | # end 35 | # 36 | # Example.new.valid? # => true 37 | # ``` 38 | # 39 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 40 | # NOTE: The type of *value* and the property must match. 41 | # NOTE: `PropertyType` can be anything that defines a `#<=` method. 42 | class Assert::Assertions::LessThanOrEqual(PropertyType) < Assert::Assertions::Assertion 43 | initializer( 44 | actual: PropertyType, 45 | value: PropertyType 46 | ) 47 | 48 | # :inherit: 49 | def default_message_template : String 50 | "'%{property_name}' should be less than or equal to '%{value}'" 51 | end 52 | 53 | # :inherit: 54 | def valid? : Bool 55 | (value = @value) && (actual = @actual) ? actual <= value : true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/assertions/greater_than_or_equal.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::GreaterThanOrEqual)] 4 | # Validates a property is greater than or equal to *value*. 5 | # 6 | # ### Example 7 | # ``` 8 | # class Example 9 | # include Assert 10 | # 11 | # def initialize; end 12 | # 13 | # @[Assert::GreaterThanOrEqual(value: 100)] 14 | # property int32 : Int32 = 100 15 | # 16 | # @[Assert::GreaterThanOrEqual(value: 0.0001_f64)] 17 | # property float : Float64 = 0.0002 18 | # 19 | # @[Assert::GreaterThanOrEqual(value: "X")] 20 | # property string : String = "Y" 21 | # 22 | # @[Assert::GreaterThanOrEqual(value: end_date)] 23 | # property start_date : Time? = Time.utc(2019, 5, 29) 24 | # 25 | # @[Assert::GreaterThanOrEqual(value: Time.utc(2019, 1, 1))] 26 | # property end_date : Time? = Time.utc(2019, 5, 20) 27 | # 28 | # @[Assert::GreaterThanOrEqual(value: max_value)] 29 | # property getter_property : UInt8 = 250_u8 30 | # 31 | # def max_value : UInt8 32 | # 250_u8 33 | # end 34 | # end 35 | # 36 | # Example.new.valid? # => true 37 | # ``` 38 | # 39 | # NOTE: *value* can be a hard-coded value like `10`, the name of another property, a constant, or the name of a method. 40 | # NOTE: The type of *value* and the property must match. 41 | # NOTE: `PropertyType` can be anything that defines a `#>=` method. 42 | class Assert::Assertions::GreaterThanOrEqual(PropertyType) < Assert::Assertions::Assertion 43 | initializer( 44 | actual: PropertyType, 45 | value: PropertyType 46 | ) 47 | 48 | # :inherit: 49 | def default_message_template : String 50 | "'%{property_name}' should be greater than or equal to '%{value}'" 51 | end 52 | 53 | # :inherit: 54 | def valid? : Bool 55 | (value = @value) && (actual = @actual) ? actual >= value : true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/exceptions/validation_error.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | # Represents a validation error. It can be raised manually or via `Assert#validate!`. 4 | class Assert::Exceptions::ValidationError < Exception 5 | def self.new(failed_assertion : Assert::Assertions::Assertion) 6 | new [failed_assertion] of Assert::Assertions::Assertion 7 | end 8 | 9 | def initialize(@failed_assertions : Array(Assert::Assertions::Assertion)) 10 | super "Validation tests failed" 11 | end 12 | 13 | # Returns a JSON/pretty JSON object for `self`. 14 | # 15 | # Can be overwritten to change the JSON schema. 16 | # ``` 17 | # error = Assert::Exceptions::ValidationError.new([ 18 | # Assert::Assertions::NotBlank(String?).new("name", ""), 19 | # Assert::Assertions::GreaterThanOrEqual(Int32).new("age", -1, 0), 20 | # ]) 21 | # 22 | # error.to_pretty_json # => 23 | # { 24 | # "code": 400, 25 | # "message": "Validation tests failed", 26 | # "errors": [ 27 | # "'name' should not be blank", 28 | # "'age' should be greater than or equal to '0'", 29 | # ], 30 | # } 31 | # ``` 32 | def to_json(builder : JSON::Builder) : Nil 33 | builder.object do 34 | builder.field "code", 400 35 | builder.field "message", @message 36 | builder.field "errors", @failed_assertions.map(&.message) 37 | end 38 | end 39 | 40 | # Returns failed validations as a string. 41 | # 42 | # ``` 43 | # error = Assert::Exceptions::ValidationError.new([ 44 | # Assert::Assertions::NotBlank(String?).new("name", ""), 45 | # Assert::Assertions::GreaterThanOrEqual(Int32).new("age", -1, 0), 46 | # ]) 47 | # 48 | # error.to_s # => "Validation tests failed: 'name' should not be blank, 'age' should be greater than or equal to '0'" 49 | # ``` 50 | def to_s : String 51 | String.build do |str| 52 | str << "Validation tests failed: " 53 | @failed_assertions.map(&.message).join(str, ", ") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/assertion_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Tesst)] 4 | # Test assertion to test common assertion behavior 5 | class TestAssertion(PropertyType) < Assert::Assertions::Assertion 6 | @some_key = "FOO" 7 | @some_bool = false 8 | @some_number = 17 9 | 10 | def initialize(property_name : String, message : String? = nil, groups : Array(String)? = nil) 11 | super property_name, message, groups 12 | end 13 | 14 | # :inherit: 15 | def default_message_template : String 16 | "DEFAULT_MESSAGE" 17 | end 18 | 19 | # :inherit: 20 | def valid? : Bool 21 | true 22 | end 23 | end 24 | 25 | describe Assert::Assertions::Assertion do 26 | describe ".new" do 27 | describe "#groups" do 28 | it "should include the default group by default" do 29 | TestAssertion(Nil).new("prop").groups.should eq ["default"] 30 | end 31 | 32 | it "should use provided groups if they were provided" do 33 | TestAssertion(Nil).new("prop", groups: ["one", "two"]).groups.should eq ["one", "two"] 34 | end 35 | end 36 | 37 | describe "#message" do 38 | it "should replace placeholders with their value" do 39 | TestAssertion(Nil).new("prop", message: "%{some_key} is %{some_bool}").message.should eq "FOO is false" 40 | end 41 | 42 | it "should allow ivars to be formatted via sprintf" do 43 | TestAssertion(Nil).new("prop", message: "%{some_number} is %0.4f").message.should eq "17 is 17.0000" 44 | end 45 | end 46 | 47 | describe "#message_template" do 48 | it "returns the default template if no message is provided" do 49 | TestAssertion(Nil).new("prop").message.should eq "DEFAULT_MESSAGE" 50 | end 51 | 52 | it "should use provided message template if one was supplied" do 53 | TestAssertion(Nil).new("prop", message: "CUSTOM_MESSAGE").message.should eq "CUSTOM_MESSAGE" 54 | end 55 | end 56 | 57 | describe "#property_name" do 58 | it "should return the property_name" do 59 | TestAssertion(Nil).new("prop").property_name.should eq "prop" 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/exceptions/validation_error_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::Exceptions::ValidationError do 4 | describe "#new" do 5 | describe Array do 6 | it "accepts an array of assertions" do 7 | Assert::Exceptions::ValidationError.new([] of Assert::Assertions::Assertion).message.should eq "Validation tests failed" 8 | end 9 | end 10 | 11 | describe Assert::Assertions::Assertion do 12 | it "accepts a single assertion" do 13 | Assert::Exceptions::ValidationError.new(Assert::Assertions::NotBlank(String?).new("name", "")).message.should eq "Validation tests failed" 14 | end 15 | end 16 | end 17 | 18 | describe "#to_json" do 19 | it "returns a JSON object representing the errors" do 20 | error = Assert::Exceptions::ValidationError.new([ 21 | Assert::Assertions::NotBlank(String?).new("name", ""), 22 | Assert::Assertions::GreaterThanOrEqual(Int32).new("age", -1, 0), 23 | ]) 24 | 25 | error.to_json.should eq %({"code":400,"message":"Validation tests failed","errors":["'name' should not be blank","'age' should be greater than or equal to '0'"]}) 26 | end 27 | end 28 | 29 | describe "#to_pretty_json" do 30 | it "returns a pretty JSON object representing the errors" do 31 | error = Assert::Exceptions::ValidationError.new([ 32 | Assert::Assertions::NotBlank(String?).new("name", ""), 33 | Assert::Assertions::GreaterThanOrEqual(Int32).new("age", -1, 0), 34 | ]) 35 | 36 | error.to_pretty_json.should eq %({\n "code": 400,\n "message": "Validation tests failed",\n "errors": [\n "'name' should not be blank",\n "'age' should be greater than or equal to '0'"\n ]\n}) 37 | end 38 | end 39 | 40 | describe "#to_s" do 41 | it "should return a string of the failed assertions" do 42 | error = Assert::Exceptions::ValidationError.new([ 43 | Assert::Assertions::NotBlank(String?).new("name", ""), 44 | Assert::Assertions::GreaterThanOrEqual(Int32).new("age", -1, 0), 45 | ]) 46 | 47 | error.to_s.should eq "Validation tests failed: 'name' should not be blank, 'age' should be greater than or equal to '0'" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/assertions/email.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Email)] 4 | # Validates a property is a properly formatted email. 5 | # 6 | # ### Optional Arguments 7 | # * *mode* - Which validation pattern to use. See `EmailValidationMode`. 8 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 9 | # 10 | # ### Example 11 | # ``` 12 | # class Example 13 | # include Assert 14 | # 15 | # def initialize; end 16 | # 17 | # @[Assert::Email] 18 | # property loose_email : String = "example@-example.com" 19 | # 20 | # @[Assert::Email(mode: :html5)] 21 | # property strict_email : String = "example@example.co.uk" 22 | # 23 | # @[Assert::Email(mode: :html5, normalizer: ->(actual : String) { actual.strip })] 24 | # property strict_email_normalizer : String = " example@example.co.uk " 25 | # end 26 | # 27 | # Example.new.valid? # => true 28 | # ``` 29 | class Assert::Assertions::Email(PropertyType) < Assert::Assertions::Assertion 30 | initializer( 31 | actual: String?, 32 | mode: "EmailValidationMode = EmailValidationMode::Loose", 33 | normalizer: "Proc(String, String)? = nil" 34 | ) 35 | 36 | # :inherit: 37 | def default_message_template : String 38 | "'%{property_name}' is not a valid email address" 39 | end 40 | 41 | # :inherit: 42 | def valid? : Bool 43 | return true unless actual = @actual 44 | !(((normalizer = @normalizer) ? normalizer.call actual : actual) =~ @mode.get_pattern).nil? 45 | end 46 | 47 | # Which validation pattern to use to validate the email. 48 | enum EmailValidationMode 49 | # A simple regular expression. Allows all values with an `@` symbol in, and a `.` in the second host part of the email address. 50 | Loose 51 | 52 | # This matches the pattern used for the [HTML5 email input element](https://www.w3.org/TR/html5/sec-forms.html#email-state-typeemail). 53 | HTML5 54 | 55 | # TODO: Validate against [RFC 5322](https://tools.ietf.org/html/rfc5322). 56 | Strict 57 | 58 | # Returns the `Regex` pattern for `self`. 59 | def get_pattern : Regex 60 | case self 61 | when .loose? then /^.+\@\S+\.\S+$/ 62 | when .html5? then /^[a-zA-Z0-9.!\#$\%&\'*+\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/ 63 | else 64 | raise NotImplementedError.new "Unsupported pattern: #{self}" 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/assertions/uuid.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | require "uuid" 3 | 4 | @[Assert::Assertions::Register(annotation: Assert::Uuid)] 5 | # Validates a string is a properly formatted [RFC4122 UUID](https://tools.ietf.org/html/rfc4122); either in hyphenated, hexstring, or urn formats. 6 | # 7 | # ### Optional Arguments 8 | # * *versions* - Only allow specific UUID versions. 9 | # * *variants* - Only allow specific UUID variants. 10 | # * *strict* - Only allow the hyphenated UUID format. 11 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 12 | # 13 | # ### Example 14 | # ``` 15 | # class Example 16 | # include Assert 17 | # 18 | # def initialize; end 19 | # 20 | # @[Assert::Uuid(strict: false)] 21 | # property hyphenless : String = "216fff4098d911e3a5e20800200c9a66" 22 | # 23 | # @[Assert::Uuid(strict: false)] 24 | # property urn : String = "urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20" 25 | # 26 | # @[Assert::Uuid] 27 | # property strict : String = "216fff40-98d9-11e3-a5e2-0800200c9a66" 28 | # 29 | # @[Assert::Uuid(versions: [UUID::Version::V1])] 30 | # property v1_only : String = "216fff40-98d9-11e3-a5e2-0800200c9a66" 31 | # 32 | # @[Assert::Uuid(variants: [UUID::Variant::Future, UUID::Variant::NCS])] 33 | # property other_variants : String = "216fff40-98d9-11e3-e5e2-0800200c9a66" 34 | # 35 | # @[Assert::Uuid(normalizer: ->(actual : String) { actual.strip })] 36 | # property normalizer : String = " 216fff40-98d9-11e3-a5e2-0800200c9a66 " 37 | # end 38 | # 39 | # Example.new.valid? # => true 40 | # ``` 41 | class Assert::Assertions::Uuid(PropertyType) < Assert::Assertions::Assertion 42 | # The indxes where a strict `UUID` should have a hyphen. 43 | HYPHEN_INDEXES = {8, 13, 18, 23} 44 | 45 | initializer( 46 | actual: String?, 47 | versions: "Array(UUID::Version) = [UUID::Version::V1, UUID::Version::V2, UUID::Version::V3, UUID::Version::V4, UUID::Version::V5]", 48 | variants: "Array(UUID::Variant) = [UUID::Variant::RFC4122]", 49 | strict: "Bool = true", 50 | normalizer: "Proc(String, String)? = nil" 51 | ) 52 | 53 | # :inherit: 54 | def default_message_template : String 55 | "'%{property_name}' is not a valid UUID" 56 | end 57 | 58 | # :inherit: 59 | def valid? : Bool 60 | return true unless actual = @actual 61 | actual = ((normalizer = @normalizer) ? normalizer.call actual : actual) 62 | return false if @strict && !HYPHEN_INDEXES.all? { |idx| actual.char_at(idx) == '-' } 63 | uuid : UUID = UUID.new actual 64 | @variants.any? { |v| v == uuid.variant } && @versions.any? { |v| v == uuid.version } 65 | rescue e : ArgumentError 66 | false 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/assertions/in_range.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::InRange)] 4 | # Validates a property is within a given `Range`. 5 | # 6 | # ### Optional Arguments 7 | # * *not_in_range_message* - Message to display if *actual* is not included in *range*. 8 | # * *min_message* - Message to display if *range* only has a minimum and *actual* is too small. 9 | # * *max_message* - Message to display if *range* only has a maximum and *actual* is too big. 10 | # 11 | # ### Example 12 | # ``` 13 | # class Example 14 | # include Assert 15 | # 16 | # def initialize; end 17 | # 18 | # @[Assert::InRange(Range(Float64, Nil), range: 0.0..)] 19 | # property minimum_only : Int32 = 27 20 | # 21 | # @[Assert::InRange(Range(Nil, Int64), range: ..100_000_000_000)] 22 | # property maximum_only : Int32 = 50_000 23 | # 24 | # @[Assert::InRange(Range(Float64, Float64), range: 0.0..1000.0)] 25 | # property range : Float64 = 3.14 26 | # 27 | # @[Assert::InRange(Range(Int32, Int32), range: 0..10, not_in_range_message: "That is not a valid %{property_name}")] 28 | # property fav_number : UInt8 = 8_u8 29 | # 30 | # @[Assert::InRange(Range(UInt8, UInt8), range: 0_u8..50_u8, min_message: "Number of cores must be positive", max_message: "There must be less than 50 cores")] 31 | # property cores : Int32 = 32 32 | # end 33 | # 34 | # Example.new.valid? # => true 35 | # ``` 36 | # NOTE: The generic `RangeType` represents the type of *range*. 37 | class Assert::Assertions::InRange(PropertyType, RangeType) < Assert::Assertions::Assertion 38 | initializer( 39 | actual: PropertyType, 40 | range: RangeType, 41 | not_in_range_message: "String? = nil", 42 | min_message: "String? = nil", 43 | max_message: "String? = nil" 44 | ) 45 | 46 | # :inherit: 47 | def default_message_template : String 48 | "'%{property_name}' is out of range" 49 | end 50 | 51 | # :inherit: 52 | # ameba:disable Metrics/CyclomaticComplexity 53 | def valid? : Bool 54 | return true unless actual = @actual 55 | return true if @range.includes? actual 56 | 57 | lower_bound = @range.begin 58 | upper_bound = @range.end 59 | 60 | @message_template = if lower_bound && upper_bound 61 | @not_in_range_message || "'%{property_name}' should be between #{lower_bound} and #{@range.excludes_end? ? upper_bound - 1 : upper_bound}" 62 | elsif upper_bound && (@range.excludes_end? ? actual >= upper_bound : actual > upper_bound) 63 | @max_message || "'%{property_name}' should be #{@range.excludes_end? ? upper_bound - 1 : upper_bound} or less" 64 | else 65 | @min_message || "'%{property_name}' should be #{lower_bound} or more" 66 | end 67 | false 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/assertions/less_than_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::LessThan do 4 | assert_template(Assert::Assertions::LessThan, "'%{property_name}' should be less than '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::LessThan, value: 10, type_vars: [Int32?]) 8 | 9 | describe "with greater than values" do 10 | describe String do 11 | it "should be invalid" do 12 | Assert::Assertions::LessThan(String).new("prop", "Y", "X").valid?.should be_false 13 | end 14 | end 15 | 16 | describe Int do 17 | it "should be invalid" do 18 | Assert::Assertions::LessThan(Int32).new("prop", 100, 50).valid?.should be_false 19 | end 20 | end 21 | 22 | describe Float do 23 | it "should be invalid" do 24 | Assert::Assertions::LessThan(Float64).new("prop", 99.5, 99.0).valid?.should be_false 25 | end 26 | end 27 | 28 | describe Time do 29 | it "should be invalid" do 30 | Assert::Assertions::LessThan(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 1)).valid?.should be_false 31 | end 32 | end 33 | end 34 | 35 | describe "with equal values" do 36 | describe String do 37 | it "should be invalid" do 38 | Assert::Assertions::LessThan(String).new("prop", "X", "X").valid?.should be_false 39 | end 40 | end 41 | 42 | describe Int do 43 | it "should be invalid" do 44 | Assert::Assertions::LessThan(Int32).new("prop", 100, 100).valid?.should be_false 45 | end 46 | end 47 | 48 | describe Float do 49 | it "should be invalid" do 50 | Assert::Assertions::LessThan(Float64).new("prop", 99.5, 99.5).valid?.should be_false 51 | end 52 | end 53 | 54 | describe Time do 55 | it "should be invalid" do 56 | Assert::Assertions::LessThan(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 3)).valid?.should be_false 57 | end 58 | end 59 | end 60 | 61 | describe "with less than values" do 62 | describe String do 63 | it "should be valid" do 64 | Assert::Assertions::LessThan(String).new("prop", "A", "Y").valid?.should be_true 65 | end 66 | end 67 | 68 | describe Int do 69 | it "should be valid" do 70 | Assert::Assertions::LessThan(Int32).new("prop", 50, 100).valid?.should be_true 71 | end 72 | end 73 | 74 | describe Float do 75 | it "should be valid" do 76 | Assert::Assertions::LessThan(Float64).new("prop", 99.0, 99.5).valid?.should be_true 77 | end 78 | end 79 | 80 | describe Time do 81 | it "should be valid" do 82 | Assert::Assertions::LessThan(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 3)).valid?.should be_true 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /src/assertions/choice.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Choice)] 4 | # Validates a property is a valid choice. 5 | # 6 | # ### Optional Arguments 7 | # * *min_matches* - Must select _at least_ *min_matches* to be valid. 8 | # * *min_message* - Message to display if too few choices are selected. 9 | # * *max_matches* - Must select _at most_ *max_matches* to be valid. 10 | # * *max_message* - Message to display if too many choices are selected. 11 | # * *multiple_message* - Message to display if one or more values in *actual* are not in *choices*. 12 | # 13 | # ### Example 14 | # ``` 15 | # class Example 16 | # include Assert 17 | # 18 | # def initialize; end 19 | # 20 | # @[Assert::Choice(choices: ["Active", "Inactive", "Pending"])] 21 | # property status : String = "Inactive" 22 | # 23 | # @[Assert::Choice(choices: [2, 4, 6], multiple_message: "One ore more value is invalid")] 24 | # property fav_numbers : Array(Int32) = [2, 4, 6] 25 | # 26 | # @[Assert::Choice(choices: ['a', 'b', 'c'], min_matches: 2, min_message: "You must have at least 2 choices")] 27 | # property fav_letters_min : Array(Char) = ['a', 'c'] 28 | # 29 | # @[Assert::Choice(choices: ['a', 'b', 'c'], max_matches: 2, max_message: "You must have at most 2 choices")] 30 | # property fav_letters_max : Array(Char) = ['a'] 31 | # end 32 | # 33 | # Example.new.valid? # => true 34 | # ``` 35 | # 36 | # NOTE: The generic `ChoicesType` represents the type of *choices*. 37 | class Assert::Assertions::Choice(PropertyType, ChoicesType) < Assert::Assertions::Assertion 38 | initializer( 39 | actual: PropertyType, 40 | choices: ChoicesType, 41 | min_matches: "Int32? = nil", 42 | max_matches: "Int32? = nil", 43 | min_message: "String? = nil", 44 | max_message: "String? = nil", 45 | multiple_message: "String? = nil" 46 | ) 47 | 48 | # :inherit: 49 | def default_message_template : String 50 | "'%{property_name}' is not a valid choice" 51 | end 52 | 53 | # :inherit: 54 | def valid? : Bool 55 | return true unless actual = @actual 56 | 57 | case actual 58 | when Array 59 | num_matches = (actual & @choices).size 60 | 61 | if min = @min_matches 62 | return true unless num_matches < min 63 | @message_template = @min_message || "'%{property_name}': You must select at least #{@min_matches} choice(s)" 64 | return false 65 | end 66 | 67 | if max = @max_matches 68 | return true unless num_matches > max 69 | @message_template = @max_message || "'%{property_name}': You must select at most #{@max_matches} choice(s)" 70 | return false 71 | end 72 | 73 | if num_matches != @choices.size 74 | @message_template = @multiple_message || "'%{property_name}': One or more of the given values is invalid" 75 | return false 76 | end 77 | 78 | true 79 | else 80 | @choices.includes? actual 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /src/assertions/ip.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Ip)] 4 | # Validates a property is a properly formatted IP address. 5 | # 6 | # ### Optional Arguments 7 | # * *version* - Which IP version to use. See `IPVersion`. 8 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 9 | # 10 | # ### Example 11 | # ``` 12 | # class Example 13 | # include Assert 14 | # 15 | # def initialize; end 16 | # 17 | # @[Assert::Ip] 18 | # property ipv4 : String = "255.255.255.255" 19 | # 20 | # @[Assert::Ip(version: :ipv6)] 21 | # property ipv6 : String = "0::0" 22 | # 23 | # @[Assert::Ip(normalizer: ->(actual : String) { actual.strip })] 24 | # property normalizer : String = " 192.168.1.1 " 25 | # end 26 | # 27 | # Example.new.valid? # => true 28 | # ``` 29 | class Assert::Assertions::Ip(PropertyType) < Assert::Assertions::Assertion 30 | initializer( 31 | actual: String?, 32 | version: "IPVersion = IPVersion::IPV4", 33 | normalizer: "Proc(String, String)? = nil" 34 | ) 35 | 36 | # :inherit: 37 | def default_message_template : String 38 | "'%{property_name}' is not a valid IP address" 39 | end 40 | 41 | # :inherit: 42 | def valid? : Bool 43 | return true unless actual = @actual 44 | !(((normalizer = @normalizer) ? normalizer.call actual : actual) =~ @version.get_pattern).nil? 45 | end 46 | 47 | # Which IP version to use to validate against. 48 | enum IPVersion 49 | # Matches IPv4 format. 50 | IPV4 51 | 52 | # Matches IPv6 format. 53 | IPV6 54 | 55 | # Returns the `Regex` pattern for `self`. 56 | def get_pattern : Regex 57 | case self 58 | when .ipv4? then /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ 59 | when .ipv6? then /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ 60 | else 61 | raise NotImplementedError.new "Unsupported version: #{self}" 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/assertions/greater_than_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::GreaterThan do 4 | assert_template(Assert::Assertions::GreaterThan, "'%{property_name}' should be greater than '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::GreaterThan, value: 10, type_vars: [Int32?]) 8 | 9 | describe "with greater than values" do 10 | describe String do 11 | it "should be valid" do 12 | Assert::Assertions::GreaterThan(String).new("prop", "Y", "X").valid?.should be_true 13 | end 14 | end 15 | 16 | describe Int do 17 | it "should be valid" do 18 | Assert::Assertions::GreaterThan(Int32).new("prop", 100, 50).valid?.should be_true 19 | end 20 | end 21 | 22 | describe Float do 23 | it "should be valid" do 24 | Assert::Assertions::GreaterThan(Float64).new("prop", 99.5, 99.0).valid?.should be_true 25 | end 26 | end 27 | 28 | describe Time do 29 | it "should be valid" do 30 | Assert::Assertions::GreaterThan(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 1)).valid?.should be_true 31 | end 32 | end 33 | end 34 | 35 | describe "with equal values" do 36 | describe String do 37 | it "should be invalid" do 38 | Assert::Assertions::GreaterThan(String).new("prop", "X", "X").valid?.should be_false 39 | end 40 | end 41 | 42 | describe Int do 43 | it "should be invalid" do 44 | Assert::Assertions::GreaterThan(Int32).new("prop", 100, 100).valid?.should be_false 45 | end 46 | end 47 | 48 | describe Float do 49 | it "should be invalid" do 50 | Assert::Assertions::GreaterThan(Float64).new("prop", 99.5, 99.5).valid?.should be_false 51 | end 52 | end 53 | 54 | describe Time do 55 | it "should be invalid" do 56 | Assert::Assertions::GreaterThan(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 3)).valid?.should be_false 57 | end 58 | end 59 | end 60 | 61 | describe "with less than values" do 62 | describe String do 63 | it "should be invalid" do 64 | Assert::Assertions::GreaterThan(String).new("prop", "A", "Y").valid?.should be_false 65 | end 66 | end 67 | 68 | describe Int do 69 | it "should be invalid" do 70 | Assert::Assertions::GreaterThan(Int32).new("prop", 50, 100).valid?.should be_false 71 | end 72 | end 73 | 74 | describe Float do 75 | it "should be invalid" do 76 | Assert::Assertions::GreaterThan(Float64).new("prop", 99.0, 99.5).valid?.should be_false 77 | end 78 | end 79 | 80 | describe Time do 81 | it "should be invalid" do 82 | Assert::Assertions::GreaterThan(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 3)).valid?.should be_false 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/assertions/less_than_or_equal_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::LessThanOrEqual do 4 | assert_template(Assert::Assertions::LessThanOrEqual, "'%{property_name}' should be less than or equal to '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::LessThanOrEqual, value: 10, type_vars: [Int32?]) 8 | 9 | describe "with greater than values" do 10 | describe String do 11 | it "should be invalid" do 12 | Assert::Assertions::LessThanOrEqual(String).new("prop", "Y", "X").valid?.should be_false 13 | end 14 | end 15 | 16 | describe Int do 17 | it "should be invalid" do 18 | Assert::Assertions::LessThanOrEqual(Int32).new("prop", 100, 50).valid?.should be_false 19 | end 20 | end 21 | 22 | describe Float do 23 | it "should be invalid" do 24 | Assert::Assertions::LessThanOrEqual(Float64).new("prop", 99.5, 99.0).valid?.should be_false 25 | end 26 | end 27 | 28 | describe Time do 29 | it "should be invalid" do 30 | Assert::Assertions::LessThanOrEqual(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 1)).valid?.should be_false 31 | end 32 | end 33 | end 34 | 35 | describe "with equal values" do 36 | describe String do 37 | it "should be valid" do 38 | Assert::Assertions::LessThanOrEqual(String).new("prop", "X", "X").valid?.should be_true 39 | end 40 | end 41 | 42 | describe Int do 43 | it "should be valid" do 44 | Assert::Assertions::LessThanOrEqual(Int32).new("prop", 100, 100).valid?.should be_true 45 | end 46 | end 47 | 48 | describe Float do 49 | it "should be valid" do 50 | Assert::Assertions::LessThanOrEqual(Float64).new("prop", 99.5, 99.5).valid?.should be_true 51 | end 52 | end 53 | 54 | describe Time do 55 | it "should be valid" do 56 | Assert::Assertions::LessThanOrEqual(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 3)).valid?.should be_true 57 | end 58 | end 59 | end 60 | 61 | describe "with less than values" do 62 | describe String do 63 | it "should be valid" do 64 | Assert::Assertions::LessThanOrEqual(String).new("prop", "A", "Y").valid?.should be_true 65 | end 66 | end 67 | 68 | describe Int do 69 | it "should be valid" do 70 | Assert::Assertions::LessThanOrEqual(Int32).new("prop", 50, 100).valid?.should be_true 71 | end 72 | end 73 | 74 | describe Float do 75 | it "should be valid" do 76 | Assert::Assertions::LessThanOrEqual(Float64).new("prop", 99.0, 99.5).valid?.should be_true 77 | end 78 | end 79 | 80 | describe Time do 81 | it "should be valid" do 82 | Assert::Assertions::LessThanOrEqual(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 3)).valid?.should be_true 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /src/assertions/size.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Size)] 4 | # Validates a property's size is within a given `Range`. 5 | # 6 | # ### Optional Arguments 7 | # * *exact_message* - Message to display if *range*'s begin and end are the same and *actual* is not that value. 8 | # * *min_message* - Message to display if *actual* is too small. 9 | # * *max_message* - Message to display if *actual* is too big. 10 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 11 | # 12 | # ### Example 13 | # ``` 14 | # class Example 15 | # include Assert 16 | # 17 | # def initialize; end 18 | # 19 | # @[Assert::Size(Range(Float64, Float64), range: 2.0..3.0)] 20 | # property fav_numbers : Array(Int32) = [1, 2, 3] 21 | # 22 | # @[Assert::Size(Range(Float64, Float64), range: 5.0..10.0, min_message: "Password should be at least 5 characters", max_message: "Password cannot be more than 10 characters")] 23 | # property password : String = "monkey12" 24 | # 25 | # @[Assert::Size(Range(Int32, Int32), range: 5..5, exact_message: "Value must be exactly 5 characters")] 26 | # property exact_value : String = "hello" 27 | # 28 | # @[Assert::Size(Range(Float64, Float64), range: 5.0..10.0, normalizer: ->(actual : String) { actual.strip })] 29 | # property normalizer : String = " crystal " 30 | # end 31 | # 32 | # Example.new.valid? # => true 33 | # ``` 34 | # NOTE: `PropertyType` can be anything that implements `#size`. 35 | # NOTE: The generic `RangeType` represents the type of *range*. 36 | class Assert::Assertions::Size(PropertyType, RangeType) < Assert::Assertions::Assertion 37 | initializer( 38 | actual: PropertyType, 39 | range: RangeType, 40 | normalizer: "Proc(PropertyType, PropertyType)? = nil", 41 | exact_message: "String? = nil", 42 | min_message: "String? = nil", 43 | max_message: "String? = nil" 44 | ) 45 | 46 | # :inherit: 47 | def default_message_template : String 48 | "'%{property_name}' is not the correct size" 49 | end 50 | 51 | # :inherit: 52 | # ameba:disable Metrics/CyclomaticComplexity 53 | def valid? : Bool 54 | return true unless (actual = (normalizer = @normalizer) ? normalizer.call @actual : @actual) 55 | return true if @range.includes? actual.size 56 | @message_template = if @range.end == @range.begin && !@range.includes? actual.size 57 | @exact_message || "'%{property_name}' is not the proper size. It should have exactly #{@range.end} #{actual.is_a?(String) ? "character(s)" : "element(s)"}" 58 | elsif @range.excludes_end? ? actual.size >= @range.end : actual.size > @range.end 59 | @max_message || "'%{property_name}' is too long. It should have #{@range.end} #{actual.is_a?(String) ? "character(s)" : "element(s)"} or less" 60 | else 61 | @min_message || "'%{property_name}' is too short. It should have #{@range.begin} #{actual.is_a?(String) ? "character(s)" : "element(s)"} or more" 62 | end 63 | false 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/assertions/greater_than_or_equal_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::GreaterThanOrEqual do 4 | assert_template(Assert::Assertions::GreaterThanOrEqual, "'%{property_name}' should be greater than or equal to '%{value}'", value: nil) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::GreaterThanOrEqual, value: 10, type_vars: [Int32?]) 8 | 9 | describe "with greater than values" do 10 | describe String do 11 | it "should be valid" do 12 | Assert::Assertions::GreaterThanOrEqual(String).new("prop", "Y", "X").valid?.should be_true 13 | end 14 | end 15 | 16 | describe Int do 17 | it "should be valid" do 18 | Assert::Assertions::GreaterThanOrEqual(Int32).new("prop", 100, 50).valid?.should be_true 19 | end 20 | end 21 | 22 | describe Float do 23 | it "should be valid" do 24 | Assert::Assertions::GreaterThanOrEqual(Float64).new("prop", 99.5, 99.0).valid?.should be_true 25 | end 26 | end 27 | 28 | describe Time do 29 | it "should be valid" do 30 | Assert::Assertions::GreaterThanOrEqual(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 1)).valid?.should be_true 31 | end 32 | end 33 | end 34 | 35 | describe "with equal values" do 36 | describe String do 37 | it "should be valid" do 38 | Assert::Assertions::GreaterThanOrEqual(String).new("prop", "X", "X").valid?.should be_true 39 | end 40 | end 41 | 42 | describe Int do 43 | it "should be valid" do 44 | Assert::Assertions::GreaterThanOrEqual(Int32).new("prop", 100, 100).valid?.should be_true 45 | end 46 | end 47 | 48 | describe Float do 49 | it "should be valid" do 50 | Assert::Assertions::GreaterThanOrEqual(Float64).new("prop", 99.5, 99.5).valid?.should be_true 51 | end 52 | end 53 | 54 | describe Time do 55 | it "should be valid" do 56 | Assert::Assertions::GreaterThanOrEqual(Time).new("prop", Time.utc(2019, 1, 3), Time.utc(2019, 1, 3)).valid?.should be_true 57 | end 58 | end 59 | end 60 | 61 | describe "with less than values" do 62 | describe String do 63 | it "should be invalid" do 64 | Assert::Assertions::GreaterThanOrEqual(String).new("prop", "A", "Y").valid?.should be_false 65 | end 66 | end 67 | 68 | describe Int do 69 | it "should be invalid" do 70 | Assert::Assertions::GreaterThanOrEqual(Int32).new("prop", 50, 100).valid?.should be_false 71 | end 72 | end 73 | 74 | describe Float do 75 | it "should be invalid" do 76 | Assert::Assertions::GreaterThanOrEqual(Float64).new("prop", 99.0, 99.5).valid?.should be_false 77 | end 78 | end 79 | 80 | describe Time do 81 | it "should be invalid" do 82 | Assert::Assertions::GreaterThanOrEqual(Time).new("prop", Time.utc(2019, 1, 1), Time.utc(2019, 1, 3)).valid?.should be_false 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/assertions/email_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | VALID_LOOSE_EMAILS = [ 4 | "blacksmoke16@eve.tools", 5 | "example@example.co.uk", 6 | "fabien_potencier@example.fr", 7 | "example@example.co..uk", 8 | "{}~!@!@£$%%^&*().!@£$%^&*()", 9 | "example@example.co..uk", 10 | "example@-example.com", 11 | "example@#{"a"*64}.com", 12 | ] 13 | 14 | INVALID_LOOSE_EMAILS = [ 15 | "example", 16 | "example@", 17 | "example@localhost", 18 | "foo@example.com bar", 19 | ] 20 | 21 | VALID_HTML5_EMAILS = [ 22 | "blacksmoke16@eve.tools", 23 | "example@example.co.uk", 24 | "blacksmoke_blacksmoke@example.fr", 25 | "{}~!@example.com", 26 | ] 27 | 28 | INVALID_HTML5_EMAILS = [ 29 | "example", 30 | "example@", 31 | "example@localhost", 32 | "example@example.co..uk", 33 | "foo@example.com bar", 34 | "example@example.", 35 | "example@.fr", 36 | "@example.com", 37 | "example@example.com;example@example.com", 38 | "example@.", 39 | " example@example.com", 40 | "example@ ", 41 | " example@example.com ", 42 | " example @example .com ", 43 | "example@-example.com", 44 | "example@#{"a"*64}.com", 45 | ] 46 | 47 | EMAILS_WITH_WHITESPACE = [ 48 | "\x20example@example.co.uk\x20", 49 | "\x09\x09example@example.co..uk\x09\x09", 50 | "\x0A{}~!@!@£$%%^&*().!@£$%^&*()\x0A", 51 | "\x0D\x0Dexample@example.co..uk\x0D\x0D", 52 | "\x00example@-example.com", 53 | "example@example.com\x0B\x0B", 54 | ] 55 | 56 | describe Assert::Email do 57 | assert_template(Assert::Assertions::Email, "'%{property_name}' is not a valid email address") 58 | 59 | describe "#valid?" do 60 | assert_nil(Assert::Assertions::Email) 61 | 62 | describe :loose do 63 | describe "with valid emails" do 64 | it "should all be valid" do 65 | VALID_LOOSE_EMAILS.each do |email| 66 | Assert::Assertions::Email(String?).new("prop", email).valid?.should be_true 67 | end 68 | end 69 | end 70 | 71 | describe "with invalid emails" do 72 | it "should all be invalid" do 73 | INVALID_LOOSE_EMAILS.each do |email| 74 | Assert::Assertions::Email(String?).new("prop", email).valid?.should be_false 75 | end 76 | end 77 | end 78 | end 79 | 80 | describe :html5 do 81 | describe "with valid emails" do 82 | it "should all be valid" do 83 | VALID_HTML5_EMAILS.each do |email| 84 | Assert::Assertions::Email(String?).new("prop", email, mode: :html5).valid?.should be_true 85 | end 86 | end 87 | end 88 | 89 | describe "with invalid emails" do 90 | it "should all be invalid" do 91 | INVALID_HTML5_EMAILS.each do |email| 92 | Assert::Assertions::Email(String?).new("prop", email, mode: :html5).valid?.should be_false 93 | end 94 | end 95 | end 96 | end 97 | 98 | describe :normalizer do 99 | it "should be normalized to be valid" do 100 | EMAILS_WITH_WHITESPACE.each do |email| 101 | Assert::Assertions::Email(String?).new("prop", email, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/assertions/ip_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | VALID_IPV4 = [ 4 | "0.0.0.0", 5 | "10.0.0.0", 6 | "123.45.67.178", 7 | "172.16.0.0", 8 | "192.168.1.0", 9 | "224.0.0.1", 10 | "255.255.255.255", 11 | "127.0.0.0", 12 | ] 13 | 14 | INVALID_IPV4 = [ 15 | "0", 16 | "0.0", 17 | "0.0.0", 18 | "256.0.0.0", 19 | "0.256.0.0", 20 | "0.0.256.0", 21 | "0.0.0.256", 22 | "-1.0.0.0", 23 | "foobar", 24 | ] 25 | 26 | VALID_IPV6 = [ 27 | "2001:0db8:85a3:0000:0000:8a2e:0370:7334", 28 | "2001:0DB8:85A3:0000:0000:8A2E:0370:7334", 29 | "2001:0Db8:85a3:0000:0000:8A2e:0370:7334", 30 | "fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c", 31 | "fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c", 32 | "fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334", 33 | "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", 34 | "fe80:0:0:0:202:b3ff:fe1e:8329", 35 | "fe80::202:b3ff:fe1e:8329", 36 | "0:0:0:0:0:0:0:0", 37 | "::", 38 | "0::", 39 | "::0", 40 | "0::0", 41 | # IPv4 mapped to IPv6 42 | "2001:0db8:85a3:0000:0000:8a2e:0.0.0.0", 43 | "::0.0.0.0", 44 | "::255.255.255.255", 45 | "::123.45.67.178", 46 | ] 47 | 48 | INVALID_IPV6 = [ 49 | "z001:0db8:85a3:0000:0000:8a2e:0370:7334", 50 | "fe80", 51 | "fe80:8329", 52 | "fe80:::202:b3ff:fe1e:8329", 53 | "fe80::202:b3ff::fe1e:8329", 54 | # IPv4 mapped to IPv6 55 | "2001:0db8:85a3:0000:0000:8a2e:0370:0.0.0.0", 56 | "::0.0", 57 | "::0.0.0", 58 | "::256.0.0.0", 59 | "::0.256.0.0", 60 | "::0.0.256.0", 61 | "::0.0.0.256", 62 | ] 63 | 64 | IPV4_WHITESPACE = [ 65 | "\x200.0.0.0", 66 | "\x09\x0910.0.0.0", 67 | "123.45.67.178\x0A", 68 | "172.16.0.0\x0D\x0D", 69 | "\x0B\x0B224.0.0.1\x0B\x0B", 70 | ] 71 | 72 | describe Assert::Ip do 73 | assert_template(Assert::Assertions::Ip, "'%{property_name}' is not a valid IP address") 74 | 75 | describe "#valid?" do 76 | assert_nil(Assert::Assertions::Ip) 77 | 78 | describe :mode do 79 | describe :ipv4 do 80 | describe "with valid ips" do 81 | it "should all be valid" do 82 | VALID_IPV4.each do |ip| 83 | Assert::Assertions::Ip(String?).new("prop", ip).valid?.should be_true 84 | end 85 | end 86 | end 87 | 88 | describe "with invalid ips" do 89 | it "should all be invalid" do 90 | INVALID_IPV4.each do |ip| 91 | Assert::Assertions::Ip(String?).new("prop", ip).valid?.should be_false 92 | end 93 | end 94 | end 95 | end 96 | 97 | describe :ipv6 do 98 | describe "with valid ips" do 99 | it "should all be valid" do 100 | VALID_IPV6.each do |ip| 101 | Assert::Assertions::Ip(String?).new("prop", ip, version: :ipv6).valid?.should be_true 102 | end 103 | end 104 | end 105 | 106 | describe "with invalid ips" do 107 | it "should all be invalid" do 108 | INVALID_IPV6.each do |ip| 109 | Assert::Assertions::Ip(String?).new("prop", ip, version: :ipv6).valid?.should be_false 110 | end 111 | end 112 | end 113 | end 114 | end 115 | 116 | describe :normalizer do 117 | it "should be normalized to be valid" do 118 | IPV4_WHITESPACE.each do |ip| 119 | Assert::Assertions::Ip(String?).new("prop", ip, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /src/assertions/url.cr: -------------------------------------------------------------------------------- 1 | require "../assertion" 2 | 3 | @[Assert::Assertions::Register(annotation: Assert::Url)] 4 | # Validates a string is a properly formatted URL. 5 | # 6 | # ### Optional Arguments 7 | # * *protocols* - The protocols considered to be valid. 8 | # * *relative_protocol* - If the protocol is optional. 9 | # * *normalizer* - Execute a `Proc` to alter *actual* before checking its validity. 10 | # 11 | # ### Example 12 | # ``` 13 | # class Example 14 | # include Assert 15 | # 16 | # def initialize; end 17 | # 18 | # @[Assert::Url] 19 | # property ipv6_url : String = "http://[::1]:80/" 20 | # 21 | # @[Assert::Url(relative_protocol: true)] 22 | # property relative_url : String = "//example.fake/blog/" 23 | # 24 | # @[Assert::Url(protocols: %w(ftp file git))] 25 | # property file_url : String = "file://127.0.0.1" 26 | # 27 | # @[Assert::Url(normalizer: ->(actual : String) { actual.strip })] 28 | # property normalizer : String = "\x09\x09http://www.google.com" 29 | # end 30 | # 31 | # Example.new.valid? # => true 32 | # ``` 33 | class Assert::Assertions::Url(PropertyType) < Assert::Assertions::Assertion 34 | initializer( 35 | actual: String?, 36 | protocols: "Array(String) = %w(http https)", 37 | relative_protocol: "Bool = false", 38 | normalizer: "Proc(String, String)? = nil" 39 | ) 40 | 41 | # :inherit: 42 | def default_message_template : String 43 | "'%{property_name}' is not a valid URL" 44 | end 45 | 46 | # :inherit: 47 | def valid? : Bool 48 | return true unless actual = @actual 49 | pattern = /^#{@relative_protocol ? "(?:(#{@protocols.join('|')}):)?" : "(#{@protocols.join('|')}):"}\/\/(([\.\pL\pN-]+:)?([\.\pL\pN-]+)@)?(([\pL\pN\pS\-\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))\])(:[0-9]+)?(?:\/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|\%\%[0-9A-Fa-f]{2})* )*(?:\? (?:[\pL\pN\-._\~!$&\'()*+,;=:@\/?]|\%\%[0-9A-Fa-f]{2})* )?(?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@\/?]|\%[0-9A-Fa-f]{2})* )?$/ix 50 | !(((normalizer = @normalizer) ? normalizer.call actual : actual) =~ pattern).nil? 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/assertions/in_range_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::InRange do 4 | assert_template(Assert::Assertions::InRange, "'%{property_name}' is out of range", range: 0.0..10.0, type_vars: [Float64?, Range(Float64, Float64)]) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::InRange, [Float64?, Range(Float64, Float64)], range: 0.0..10.0) 8 | 9 | describe :not_in_range_message do 10 | describe "too small" do 11 | it "should be invalid" do 12 | assertion = Assert::Assertions::InRange(Float64?, Range(Float64, Float64)).new("prop", 2.0, range: 5.0..10.0) 13 | assertion.valid?.should be_false 14 | assertion.message.should eq "'prop' should be between 5.0 and 10.0" 15 | end 16 | end 17 | 18 | describe "too big" do 19 | it "should be invalid" do 20 | assertion = Assert::Assertions::InRange(Float64?, Range(Float64, Float64)).new("prop", 11.0, range: 5.0..10.0) 21 | assertion.valid?.should be_false 22 | assertion.message.should eq "'prop' should be between 5.0 and 10.0" 23 | end 24 | end 25 | 26 | describe "equal" do 27 | it "should be valid" do 28 | Assert::Assertions::InRange(Float64?, Range(Float64, Float64)).new("prop", 10.0, range: 5.0..10.0).valid?.should be_true 29 | end 30 | end 31 | 32 | describe "too big excluding end" do 33 | it "should be invalid" do 34 | assertion = Assert::Assertions::InRange(Float64?, Range(Float64, Float64)).new("prop", 10.0, range: 5.0...10.0) 35 | assertion.valid?.should be_false 36 | assertion.message.should eq "'prop' should be between 5.0 and 9.0" 37 | end 38 | end 39 | 40 | describe "with a custom message" do 41 | it "should be invalid" do 42 | assertion = Assert::Assertions::InRange(Float64?, Range(Float64, Float64)).new("prop", 2.0, range: 5.0..10.0, not_in_range_message: "NOT VALID") 43 | assertion.valid?.should be_false 44 | assertion.message.should eq "NOT VALID" 45 | end 46 | end 47 | end 48 | 49 | describe :min_message do 50 | describe "too small" do 51 | it "should be invalid" do 52 | assertion = Assert::Assertions::InRange(Int32?, Range(Int32, Nil)).new("prop", 2, range: 5..) 53 | assertion.valid?.should be_false 54 | assertion.message.should eq "'prop' should be 5 or more" 55 | end 56 | end 57 | 58 | describe "equal" do 59 | it "should be valid" do 60 | Assert::Assertions::InRange(Float64?, Range(Float64, Nil)).new("prop", 5.0, range: 5.0..).valid?.should be_true 61 | end 62 | end 63 | 64 | describe "with a custom message" do 65 | it "should be invalid" do 66 | assertion = Assert::Assertions::InRange(Float64?, Range(Float64, Nil)).new("prop", 2.0, range: 5.0.., min_message: "NOT VALID") 67 | assertion.valid?.should be_false 68 | assertion.message.should eq "NOT VALID" 69 | end 70 | end 71 | end 72 | 73 | describe :max_message do 74 | describe "too big" do 75 | it "should be invalid" do 76 | assertion = Assert::Assertions::InRange(Float64?, Range(Nil, Float64)).new("prop", 101.0, range: ..100.0) 77 | assertion.valid?.should be_false 78 | assertion.message.should eq "'prop' should be 100.0 or less" 79 | end 80 | end 81 | 82 | describe "equal" do 83 | it "should be valid" do 84 | Assert::Assertions::InRange(Float64?, Range(Nil, Float64)).new("prop", 100.0, range: ..100.0).valid?.should be_true 85 | end 86 | end 87 | 88 | describe "equal excluding end" do 89 | it "should be valid" do 90 | assertion = Assert::Assertions::InRange(Float64?, Range(Nil, Float64)).new("prop", 100.0, range: ...100.0) 91 | assertion.valid?.should be_false 92 | assertion.message.should eq "'prop' should be 99.0 or less" 93 | end 94 | end 95 | 96 | describe "with a custom message" do 97 | it "should be invalid" do 98 | assertion = Assert::Assertions::InRange(Float64?, Range(Nil, Float64)).new("prop", 101.0, range: ..100.0, max_message: "NOT VALID") 99 | assertion.valid?.should be_false 100 | assertion.message.should eq "NOT VALID" 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/assertions/choice_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::Choice do 4 | assert_template(Assert::Assertions::Choice, "'%{property_name}' is not a valid choice", choices: ["one", "two"], type_vars: [String?, Array(String)]) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::Choice, choices: ["one", "two"], type_vars: [String?, Array(String)]) 8 | 9 | describe "non array value" do 10 | describe "that is a valid choice" do 11 | it "should be valid" do 12 | Assert::Assertions::Choice(String?, Array(String)).new("prop", "Inactive", ["Inactive", "Active", "Pending"]).valid?.should be_true 13 | end 14 | end 15 | 16 | describe "that is not a valid choice" do 17 | it "should be invalid" do 18 | Assert::Assertions::Choice(String?, Array(String)).new("prop", "Expired", ["Inactive", "Active", "Pending"]).valid?.should be_false 19 | end 20 | end 21 | end 22 | 23 | describe Array do 24 | describe "that is a valid choice" do 25 | it "should be valid" do 26 | Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3, 4], [2, 3, 4]).valid?.should be_true 27 | end 28 | end 29 | 30 | describe :multiple_message do 31 | describe "without a custom message" do 32 | it "should use the default" do 33 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2], [2, 3, 4]) 34 | assertion.valid?.should be_false 35 | assertion.message.should eq "'prop': One or more of the given values is invalid" 36 | end 37 | end 38 | 39 | describe "with a custom message" do 40 | it "should usse that message" do 41 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2], [2, 3, 4], multiple_message: "Invalid Choices") 42 | assertion.valid?.should be_false 43 | assertion.message.should eq "Invalid Choices" 44 | end 45 | end 46 | end 47 | 48 | describe :min_matches do 49 | describe "that has at least that many matches" do 50 | it "should be valid" do 51 | Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3], [2, 3, 4], min_matches: 2).valid?.should be_true 52 | end 53 | end 54 | 55 | describe "that has more matches" do 56 | it "should be valid" do 57 | Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3, 4, 5], [2, 3, 4, 5, 6], min_matches: 2).valid?.should be_true 58 | end 59 | end 60 | 61 | describe "that has less matches" do 62 | describe "without a custom message" do 63 | it "should use the default" do 64 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2], [2, 3, 4], min_matches: 2) 65 | assertion.valid?.should be_false 66 | assertion.message.should eq "'prop': You must select at least 2 choice(s)" 67 | end 68 | end 69 | 70 | describe "with a custom message" do 71 | it "should usse that message" do 72 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2], [2, 3, 4], min_matches: 2, min_message: "Too few choices") 73 | assertion.valid?.should be_false 74 | assertion.message.should eq "Too few choices" 75 | end 76 | end 77 | end 78 | end 79 | 80 | describe :max_matches do 81 | describe "that has at least that many matches" do 82 | it "should be valid" do 83 | Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3], [2, 3, 4], max_matches: 2).valid?.should be_true 84 | end 85 | end 86 | 87 | describe "that has less matches" do 88 | it "should be valid" do 89 | Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2], [2, 3, 4, 5, 6], max_matches: 2).valid?.should be_true 90 | end 91 | end 92 | 93 | describe "that has more matches" do 94 | describe "without a custom message" do 95 | it "should use the default" do 96 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3, 4], [2, 3, 4, 5], max_matches: 2) 97 | assertion.valid?.should be_false 98 | assertion.message.should eq "'prop': You must select at most 2 choice(s)" 99 | end 100 | end 101 | 102 | describe "with a custom message" do 103 | it "should usse that message" do 104 | assertion = Assert::Assertions::Choice(Array(Int32), Array(Int32)).new("prop", [2, 3, 4], [2, 3, 4, 5], max_matches: 2, max_message: "Too many choices") 105 | assertion.valid?.should be_false 106 | assertion.message.should eq "Too many choices" 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/assertions/url_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | VALID_URLS = [ 4 | "http://a.pl", 5 | "http://www.google.com", 6 | "http://www.google.com.", 7 | "http://www.google.museum", 8 | "https://google.com/", 9 | "https://google.com:80/", 10 | "http://www.example.coop/", 11 | "http://www.test-example.com/", 12 | "http://www.example.com/", 13 | "http://example.fake/blog/", 14 | "http://example.com/?", 15 | "http://example.com/search?type=&q=url+validator", 16 | "http://example.com/#", 17 | "http://example.com/#?", 18 | "http://www.example.com/doc/current/book/validation.html#supported-constraints", 19 | "http://very.long.domain.name.com/", 20 | "http://localhost/", 21 | "http://myhost123/", 22 | "http://127.0.0.1/", 23 | "http://127.0.0.1:80/", 24 | "http://[::1]/", 25 | "http://[::1]:80/", 26 | "http://[1:2:3::4:5:6:7]/", 27 | "http://sãopaulo.com/", 28 | "http://xn--sopaulo-xwa.com/", 29 | "http://sãopaulo.com.br/", 30 | "http://xn--sopaulo-xwa.com.br/", 31 | "http://пример.испытание/", 32 | "http://xn--e1afmkfd.xn--80akhbyknj4f/", 33 | "http://مثال.إختبار/", 34 | "http://xn--mgbh0fb.xn--kgbechtv/", 35 | "http://例子.测试/", 36 | "http://xn--fsqu00a.xn--0zwm56d/", 37 | "http://例子.測試/", 38 | "http://xn--fsqu00a.xn--g6w251d/", 39 | "http://例え.テスト/", 40 | "http://xn--r8jz45g.xn--zckzah/", 41 | "http://مثال.آزمایشی/", 42 | "http://xn--mgbh0fb.xn--hgbk6aj7f53bba/", 43 | "http://실례.테스트/", 44 | "http://xn--9n2bp8q.xn--9t4b11yi5a/", 45 | "http://العربية.idn.icann.org/", 46 | "http://xn--ogb.idn.icann.org/", 47 | "http://xn--e1afmkfd.xn--80akhbyknj4f.xn--e1afmkfd/", 48 | "http://xn--espaa-rta.xn--ca-ol-fsay5a/", 49 | "http://xn--d1abbgf6aiiy.xn--p1ai/", 50 | "http://☎.com/", 51 | "http://username:password@example.com", 52 | "http://user.name:password@example.com", 53 | "http://username:pass.word@example.com", 54 | "http://user.name:pass.word@example.com", 55 | "http://user-name@example.com", 56 | "http://example.com?", 57 | "http://example.com?query=1", 58 | "http://example.com/?query=1", 59 | "http://example.com#", 60 | "http://example.com#fragment", 61 | "http://example.com/#fragment", 62 | "http://example.com/#one_more%20tes", 63 | ] 64 | 65 | INVALID_URLS = [ 66 | "google.com", 67 | "://google.com", 68 | "http ://google.com", 69 | "http:/google.com", 70 | "http://goog_le.com", 71 | "http://google.com::aa", 72 | "http://google.com:aa", 73 | "ftp://google.fr", 74 | "faked://google.fr", 75 | "http://127.0.0.1:aa/", 76 | "ftp://[::1]/", 77 | "http://[::1", 78 | "http://hello.☎/", 79 | "http://:password@example.com", 80 | "http://:password@@example.com", 81 | "http://username:passwordexample.com", 82 | "http://usern@me:password@example.com", 83 | "http://example.com/exploit.html?", 84 | "http://example.com/exploit.html?hel lo", 85 | "http://example.com/exploit.html?not_a%hex", 86 | "http://", 87 | "ftp://google.com", 88 | "file://127.0.0.1", 89 | "git://[::1]/", 90 | ] 91 | 92 | VALID_CUSTOM_PROTOCOLS = [ 93 | "ftp://google.com", 94 | "file://127.0.0.1", 95 | "git://[::1]/", 96 | ] 97 | 98 | VALID_RELATIVE_URLS = [ 99 | "//google.com", 100 | "//example.fake/blog/", 101 | "//example.com/search?type=&q=url+validator", 102 | ] 103 | 104 | INVALID_RELATIVE_URLS = [ 105 | "/google.com", 106 | "//goog_le.com", 107 | "//google.com::aa", 108 | "//google.com:aa", 109 | "//127.0.0.1:aa/", 110 | "//[::1", 111 | "//hello.☎/", 112 | "//:password@example.com", 113 | "//:password@@example.com", 114 | "//username:passwordexample.com", 115 | "//usern@me:password@example.com", 116 | "//example.com/exploit.html?", 117 | "//example.com/exploit.html?hel lo", 118 | "//example.com/exploit.html?not_a%hex", 119 | "//", 120 | ] 121 | 122 | URLS_WITH_WHITESPACE = [ 123 | "\x20http://www.google.com", 124 | "\x09\x09http://www.google.com.", 125 | "http://symfony.fake/blog/\x0A", 126 | "http://symfony.com/search?type=&q=url+validator\x0D\x0D", 127 | "\x0B\x0Bhttp://username:password@symfony.com\x0B\x0B", 128 | ] 129 | 130 | describe Assert::Url do 131 | assert_template(Assert::Assertions::Url, "'%{property_name}' is not a valid URL") 132 | 133 | describe "#valid?" do 134 | assert_nil(Assert::Assertions::Url) 135 | 136 | describe :non_relative_protocol do 137 | describe "with valid urls" do 138 | it "should all be valid" do 139 | VALID_URLS.each do |url| 140 | Assert::Assertions::Url(String?).new("prop", url).valid?.should be_true 141 | end 142 | end 143 | end 144 | 145 | describe "with invalid urls" do 146 | it "should all be invalid" do 147 | INVALID_URLS.each do |url| 148 | Assert::Assertions::Url(String?).new("prop", url).valid?.should be_false 149 | end 150 | end 151 | end 152 | end 153 | 154 | describe :relative_protocol do 155 | describe "with valid relative urls" do 156 | it "should all be valid" do 157 | VALID_RELATIVE_URLS.each do |url| 158 | Assert::Assertions::Url(String?).new("prop", url, relative_protocol: true).valid?.should be_true 159 | end 160 | end 161 | end 162 | 163 | describe "with invalid relative urls" do 164 | it "should all be invalid" do 165 | INVALID_RELATIVE_URLS.each do |url| 166 | Assert::Assertions::Url(String?).new("prop", url, relative_protocol: true).valid?.should be_false 167 | end 168 | end 169 | end 170 | end 171 | 172 | describe :protocols do 173 | it "should all be valid" do 174 | VALID_CUSTOM_PROTOCOLS.each do |url| 175 | Assert::Assertions::Url(String?).new("prop", url, protocols: %w(ftp file git)).valid?.should be_true 176 | end 177 | end 178 | end 179 | 180 | describe :normalizer do 181 | it "should be normalized to be valid" do 182 | URLS_WITH_WHITESPACE.each do |url| 183 | Assert::Assertions::Url(String?).new("prop", url, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 184 | end 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/assertions/uuid_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | VALID_V1_UUID = [ 4 | "216fff40-98d9-11e3-a5e2-0800200c9a66", 5 | "216FFF40-98D9-11E3-A5E2-0800200C9A66", 6 | ] 7 | 8 | VALID_V4_UUID = [ 9 | "456daefb-5aa6-41b5-8dbc-068b05a8b201", 10 | "456DAEFb-5AA6-41B5-8DBC-068b05a8B201", 11 | ] 12 | VALID_NON_STRICT_UUID = [ 13 | "216fff4098d911e3a5e20800200c9a66", 14 | "urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20", 15 | ] + VALID_V1_UUID + VALID_V4_UUID 16 | 17 | INVALID_NON_STRICT_UUID = [ 18 | "216fff40-98d9-11e3-a5e2_0800200c9a66", 19 | "216gff40-98d9-11e3-a5e2-0800200c9a66", 20 | "216Gff40-98d9-11e3-a5e2-0800200c9a66", 21 | "216fff40-98d9-11e3-a5e2_0800200c9a6", 22 | "216fff40-98d9-11e3-a5e-20800200c9a66", 23 | "216fff40-98d9-11e3-a5e2-0800200c9a6", 24 | "216fff40-98d9-11e3-a5e2-0800200c9a666", 25 | ] 26 | 27 | VALID_STRICT_UUID = [ 28 | "216fff40-98d9-11e3-a5e2-0800200c9a66", 29 | "216fff40-98d9-11e3-a5e2-0800200c9a66", 30 | "216FFF40-98D9-11E3-A5E2-0800200C9A66", 31 | "456daefb-5aa6-41b5-8dbc-068b05a8b201", 32 | "456daEFb-5AA6-41B5-8DBC-068B05A8B201", 33 | "456daEFb-5AA6-41B5-8DBC-068B05A8B201", 34 | ] 35 | 36 | INVALID_STRICT_UUID = [ 37 | "216fff40-98d9-11e3-a5e2_0800200c9a66", 38 | "216gff40-98d9-11e3-a5e2-0800200c9a66", 39 | "216Gff40-98d9-11e3-a5e2-0800200c9a66", 40 | "216fff40-98d9-11e3-a5e-20800200c9a66", 41 | "216f-ff40-98d9-11e3-a5e2-0800200c9a66", 42 | "216fff40-98d9-11e3-a5e2-0800-200c9a66", 43 | "216fff40-98d9-11e3-a5e2-0800200c-9a66", 44 | "216fff40-98d9-11e3-a5e20800200c9a66", 45 | "216fff4098d911e3a5e20800200c9a66", 46 | "216fff40-98d9-11e3-a5e2-0800200c9a6", 47 | "216fff40-98d9-11e3-a5e2-0800200c9a666", 48 | "216fff40-98d9-01e3-a5e2-0800200c9a66", 49 | "216fff40-98d9-61e3-a5e2-0800200c9a66", 50 | "216fff40-98d9-71e3-a5e2-0800200c9a66", 51 | "216fff40-98d9-81e3-a5e2-0800200c9a66", 52 | "216fff40-98d9-91e3-a5e2-0800200c9a66", 53 | "216fff40-98d9-a1e3-a5e2-0800200c9a66", 54 | "216fff40-98d9-b1e3-a5e2-0800200c9a66", 55 | "216fff40-98d9-c1e3-a5e2-0800200c9a66", 56 | "216fff40-98d9-d1e3-a5e2-0800200c9a66", 57 | "216fff40-98d9-e1e3-a5e2-0800200c9a66", 58 | "216fff40-98d9-f1e3-a5e2-0800200c9a66", 59 | "216fff40-98d9-11e3-05e2-0800200c9a66", 60 | "216fff40-98d9-11e3-15e2-0800200c9a66", 61 | "216fff40-98d9-11e3-25e2-0800200c9a66", 62 | "216fff40-98d9-11e3-35e2-0800200c9a66", 63 | "216fff40-98d9-11e3-45e2-0800200c9a66", 64 | "216fff40-98d9-11e3-55e2-0800200c9a66", 65 | "216fff40-98d9-11e3-65e2-0800200c9a66", 66 | "216fff40-98d9-11e3-75e2-0800200c9a66", 67 | "216fff40-98d9-11e3-c5e2-0800200c9a66", 68 | "216fff40-98d9-11e3-d5e2-0800200c9a66", 69 | "216fff40-98d9-11e3-e5e2-0800200c9a66", 70 | "216fff40-98d9-11e3-f5e2-0800200c9a66", 71 | "[216fff40-98d9-11e3-a5e2-0800200c9a66]", 72 | "{216fff40-98d9-11e3-a5e2-0800200c9a66}", 73 | "urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20", 74 | "216f-ff40-98d9-11e3-a5e2-0800-200c-9a66", 75 | "216fff40-98d911e3-a5e20800-200c9a66", 76 | ] 77 | 78 | INVALID_OTHER_VARIANT_UUID = [ 79 | "216fff40-98d9-11e3-a5e2-0800200c9a66", 80 | "216fff40-98d9-11e3-a5e2-0800200c9a66", 81 | "216FFF40-98D9-11E3-A5E2-0800200C9A66", 82 | ] 83 | 84 | VALID_OTHER_VARIANT_UUID = [ 85 | "216fff40-98d9-11e3-45e2-0800200c9a66", 86 | "216fff40-98d9-11e3-e5e2-0800200c9a66", 87 | ] 88 | 89 | STRICT_UUIDS_WITH_WHITESPACE = [ 90 | "\x20216fff40-98d9-11e3-a5e2-0800200c9a66", 91 | "\x09\x09216fff40-98d9-11e3-a5e2-0800200c9a66", 92 | "216FFF40-98D9-11E3-A5E2-0800200C9A66\x0A", 93 | "456daefb-5aa6-41b5-8dbc-068b05a8b201\x0D\x0D", 94 | "\x0B\x0B456daEFb-5AA6-41B5-8DBC-068B05A8B201\x0B\x0B", 95 | ] 96 | 97 | describe Assert::Email do 98 | assert_template(Assert::Assertions::Uuid, "'%{property_name}' is not a valid UUID") 99 | 100 | describe "#valid?" do 101 | assert_nil(Assert::Assertions::Uuid) 102 | 103 | describe :strict do 104 | describe "with valid uuids" do 105 | it "should all be valid" do 106 | VALID_STRICT_UUID.each do |uuid| 107 | Assert::Assertions::Uuid(String?).new("prop", uuid).valid?.should be_true 108 | end 109 | end 110 | end 111 | 112 | describe "with invalid uuids" do 113 | it "should all be invalid" do 114 | INVALID_STRICT_UUID.each do |uuid| 115 | Assert::Assertions::Uuid(String?).new("prop", uuid).valid?.should be_false 116 | end 117 | end 118 | end 119 | end 120 | 121 | describe :non_strict do 122 | describe "with valid uuids" do 123 | it "should all be valid" do 124 | VALID_NON_STRICT_UUID.each do |uuid| 125 | Assert::Assertions::Uuid(String?).new("prop", uuid, strict: false).valid?.should be_true 126 | end 127 | end 128 | end 129 | 130 | describe "with invalid uuids" do 131 | it "should all be invalid" do 132 | INVALID_NON_STRICT_UUID.each do |uuid| 133 | Assert::Assertions::Uuid(String?).new("prop", uuid, strict: false).valid?.should be_false 134 | end 135 | end 136 | end 137 | end 138 | 139 | describe :versions do 140 | describe "with valid uuids" do 141 | it "should all be valid" do 142 | VALID_V4_UUID.each do |uuid| 143 | Assert::Assertions::Uuid(String?).new("prop", uuid, versions: [UUID::Version::V3, UUID::Version::V4]).valid?.should be_true 144 | end 145 | end 146 | end 147 | 148 | describe "with invalid uuids" do 149 | it "should all be invalid" do 150 | VALID_V1_UUID.each do |uuid| 151 | Assert::Assertions::Uuid(String?).new("prop", uuid, versions: [UUID::Version::V3, UUID::Version::V4]).valid?.should be_false 152 | end 153 | end 154 | end 155 | end 156 | 157 | describe :variants do 158 | describe "with valid uuids" do 159 | it "should all be valid" do 160 | VALID_OTHER_VARIANT_UUID.each do |uuid| 161 | Assert::Assertions::Uuid(String?).new("prop", uuid, variants: [UUID::Variant::Future, UUID::Variant::NCS]).valid?.should be_true 162 | end 163 | end 164 | end 165 | 166 | describe "with invalid uuids" do 167 | it "should all be invalid" do 168 | INVALID_OTHER_VARIANT_UUID.each do |uuid| 169 | Assert::Assertions::Uuid(String?).new("prop", uuid, variants: [UUID::Variant::Future, UUID::Variant::NCS]).valid?.should be_false 170 | end 171 | end 172 | end 173 | end 174 | 175 | describe :normalizer do 176 | it "should be normalized to be valid" do 177 | STRICT_UUIDS_WITH_WHITESPACE.each do |uuid| 178 | Assert::Assertions::Uuid(String?).new("prop", uuid, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 179 | end 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /spec/assertions/size_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Assert::Size do 4 | assert_template(Assert::Assertions::Size, "'%{property_name}' is not the correct size", range: 0.0..10.0, type_vars: [String?, Range(Float64, Float64)]) 5 | 6 | describe "#valid?" do 7 | assert_nil(Assert::Assertions::Size, range: 0.0..10.0, type_vars: [String?, Range(Float64, Float64)]) 8 | 9 | describe String do 10 | describe "with a valid range" do 11 | it "should be valid" do 12 | Assert::Assertions::Size(String?, Range(Float64, Float64)).new("prop", "hello", range: 0.0..10.0).valid?.should be_true 13 | end 14 | end 15 | 16 | describe "with a valid exact range" do 17 | it "should be valid" do 18 | Assert::Assertions::Size(String?, Range(Int32, Int32)).new("prop", "hello", range: 5..5).valid?.should be_true 19 | end 20 | end 21 | 22 | describe "with an invalid inclusive range" do 23 | it "should be valid" do 24 | assertion = Assert::Assertions::Size(String?, Range(Int32, Int32)).new("prop", "hello", range: 5...5) 25 | assertion.valid?.should be_false 26 | assertion.message.should eq "'prop' is not the proper size. It should have exactly 5 character(s)" 27 | end 28 | end 29 | 30 | describe "too long" do 31 | describe "with the default template" do 32 | it "should be invalid and use the default template" do 33 | assertion = Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", "goodbye", range: 1.0..5.0) 34 | assertion.valid?.should be_false 35 | assertion.message.should eq "'prop' is too long. It should have 5.0 character(s) or less" 36 | end 37 | end 38 | 39 | describe "with a custom template" do 40 | it "should be invalid and use the provided template" do 41 | assertion = Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", "goodbye", range: 1.0..5.0, max_message: "Too Long") 42 | assertion.valid?.should be_false 43 | assertion.message.should eq "Too Long" 44 | end 45 | end 46 | end 47 | 48 | describe "too short" do 49 | describe "with the default template" do 50 | it "should be invalid and use the default template" do 51 | assertion = Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", "", range: 1.0..5.0) 52 | assertion.valid?.should be_false 53 | assertion.message.should eq "'prop' is too short. It should have 1.0 character(s) or more" 54 | end 55 | end 56 | 57 | describe "with a custom template" do 58 | it "should be invalid and use the provided template" do 59 | assertion = Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", "", range: 1.0..5.0, min_message: "Too Short") 60 | assertion.valid?.should be_false 61 | assertion.message.should eq "Too Short" 62 | end 63 | end 64 | end 65 | 66 | describe "not exact" do 67 | describe "with the default template" do 68 | it "should be invalid and use the default template" do 69 | assertion = Assert::Assertions::Size(String, Range(Int32, Int32)).new("prop", "foo", range: 5..5) 70 | assertion.valid?.should be_false 71 | assertion.message.should eq "'prop' is not the proper size. It should have exactly 5 character(s)" 72 | end 73 | end 74 | 75 | describe "with a custom template" do 76 | it "should be invalid and use the provided template" do 77 | assertion = Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", "foo", range: 5.0..5.0, exact_message: "Not Right") 78 | assertion.valid?.should be_false 79 | assertion.message.should eq "Not Right" 80 | end 81 | end 82 | end 83 | end 84 | 85 | describe Array do 86 | describe "with a valid range" do 87 | it "should be valid" do 88 | Assert::Assertions::Size(Array(Int32), Range(Int64, Int64)).new("prop", [1, 2, 3], range: 1_i64..5_i64).valid?.should be_true 89 | end 90 | end 91 | 92 | describe "too long" do 93 | describe "with the default template" do 94 | it "should be invalid and use the default template" do 95 | assertion = Assert::Assertions::Size(Array(Int32), Range(Int32, Int32)).new("prop", [1, 2, 3, 4, 5, 6], range: 1..5) 96 | assertion.valid?.should be_false 97 | assertion.message.should eq "'prop' is too long. It should have 5 element(s) or less" 98 | end 99 | end 100 | 101 | describe "with a custom template" do 102 | it "should be invalid and use the provided template" do 103 | assertion = Assert::Assertions::Size(Array(Int32), Range(Int32, Int32)).new("prop", [1, 2, 3, 4, 5, 6], range: 1..5, max_message: "Too Long") 104 | assertion.valid?.should be_false 105 | assertion.message.should eq "Too Long" 106 | end 107 | end 108 | end 109 | 110 | describe "too short" do 111 | describe "with the default template" do 112 | it "should be invalid and use the default template" do 113 | assertion = Assert::Assertions::Size(Array(Int32), Range(Float64, Float64)).new("prop", [] of Int32, range: 1.0..5.0) 114 | assertion.valid?.should be_false 115 | assertion.message.should eq "'prop' is too short. It should have 1.0 element(s) or more" 116 | end 117 | end 118 | 119 | describe "with a custom template" do 120 | it "should be invalid and use the provided template" do 121 | assertion = Assert::Assertions::Size(Array(Int32), Range(Float64, Float64)).new("prop", [] of Int32, range: 1.0..5.0, min_message: "Too Short") 122 | assertion.valid?.should be_false 123 | assertion.message.should eq "Too Short" 124 | end 125 | end 126 | end 127 | 128 | describe "not exact" do 129 | describe "with the default template" do 130 | it "should be invalid and use the default template" do 131 | assertion = Assert::Assertions::Size(Array(Int32), Range(Float64, Float64)).new("prop", [1, 2, 3] of Int32, range: 5.0..5.0) 132 | assertion.valid?.should be_false 133 | assertion.message.should eq "'prop' is not the proper size. It should have exactly 5.0 element(s)" 134 | end 135 | end 136 | 137 | describe "with a custom template" do 138 | it "should be invalid and use the provided template" do 139 | assertion = Assert::Assertions::Size(Array(Int32), Range(Float64, Float64)).new("prop", [1, 2, 3] of Int32, range: 5.0..5.0, exact_message: "Not Right") 140 | assertion.valid?.should be_false 141 | assertion.message.should eq "Not Right" 142 | end 143 | end 144 | end 145 | end 146 | 147 | describe :normalizer do 148 | it "should alter the value before checking its validity" do 149 | Assert::Assertions::Size(String, Range(Float64, Float64)).new("prop", " foo ", range: 1.0..5.0, normalizer: ->(actual : String) { actual.strip }).valid?.should be_true 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /src/assert.cr: -------------------------------------------------------------------------------- 1 | require "./assertion" 2 | require "./assertions/*" 3 | require "./exceptions/*" 4 | 5 | # Annotation based object validation library. 6 | # 7 | # See the `Assert::Assertions` namespace for the full assertion list as well as each assertion class for more detailed information/examples. 8 | # 9 | # See `Assert::Assertions::Assertion` for common/high level assertion usage documentation. 10 | # 11 | # ### Example Usage 12 | # 13 | # `Assert` supports both object based validations via annotations as well as ad hoc value validations via class methods. 14 | # #### Object Validation 15 | # 16 | # ``` 17 | # require "assert" 18 | # 19 | # class User 20 | # include Assert 21 | # 22 | # def initialize(@name : String, @age : Int32?, @email : String, @password : String); end 23 | # 24 | # # Assert their name is not blank 25 | # @[Assert::NotBlank] 26 | # property name : String 27 | # 28 | # # Asserts that their age is >= 0 AND not nil 29 | # @[Assert::NotNil] 30 | # @[Assert::GreaterThanOrEqual(value: 0)] 31 | # property age : Int32? 32 | # 33 | # # Assert their email is not blank AND is a valid format 34 | # @[Assert::Email(message: "'%{actual}' is not a proper email")] 35 | # @[Assert::NotBlank] 36 | # property email : String 37 | # 38 | # # Assert their password is between 7 and 25 characters 39 | # @[Assert::Size(Range(Int32, Int32), range: 7..25)] 40 | # property password : String 41 | # end 42 | # 43 | # user = User.new "Jim", 19, "test@email.com", "monkey123" 44 | # 45 | # # #valid? returns `true` if `self` is valid, otherwise `false` 46 | # user.valid? # => true 47 | # 48 | # user.email = "foobar" 49 | # user.password = "hi" 50 | # 51 | # # #valid? returns `true` if `self` is valid, otherwise `false` 52 | # user.valid? # => false 53 | # 54 | # # #validate returns an array of assertions that were not valid 55 | # user.validate.empty? # => false 56 | # 57 | # begin 58 | # # #validate! raises an exception if `self` is not valid 59 | # user.validate! 60 | # rescue ex : Assert::Exceptions::ValidationError 61 | # ex.to_s # => Validation tests failed: 'foobar' is not a proper email, 'password' is too short. It should have 7 character(s) or more 62 | # ex.to_json # => {"code":400,"message":"Validation tests failed","errors":["'foobar' is not a proper email","'password' is too short. It should have 7 character(s) or more"]} 63 | # end 64 | # ``` 65 | # 66 | # #### Ad Hoc Validation 67 | # ``` 68 | # # Each assertion automatically defines a shortcut class method for ad hoc validations. 69 | # Assert.not_blank "foo" # => true 70 | # Assert.not_blank "" # => false 71 | # 72 | # begin 73 | # # The bang version will raise if the value is invalid. 74 | # Assert.not_blank! " " 75 | # rescue ex 76 | # ex.to_s # => Validation tests failed: 'actual' should not be blank 77 | # end 78 | # 79 | # begin 80 | # # Optional arguments can be used just like the annotation versions. 81 | # Assert.equal_to! 15, 20, message: "%{actual} does not equal %{value}" 82 | # rescue ex 83 | # ex.to_s # => Validation tests failed: 15 does not equal 20 84 | # end 85 | # ``` 86 | module Assert 87 | # Define the Assertion annotations. 88 | macro finished 89 | {% verbatim do %} 90 | {% begin %} 91 | {% for assertion in Assert::Assertions::Assertion.subclasses %} 92 | {% ann = assertion.annotation(Assert::Assertions::Register) %} 93 | {% raise "#{assertion.name} must apply the `Assert::Assertions::Register` annotation." unless ann %} 94 | # :nodoc: 95 | annotation {{ann[:annotation].id}}; end 96 | 97 | {% initializer = assertion.methods.find &.name.==("initialize") %} 98 | {% method_name = ann[:annotation].stringify.split("::").last.underscore.id %} 99 | {% method_args = initializer.args[1..-1] %} 100 | {% method_vars = initializer.args[1..-1].map(&.name).splat %} 101 | {% free_variables = assertion.type_vars %} 102 | {% generic_args = free_variables.size > 1 ? ",#{free_variables[1..-1].splat}".id : "".id %} 103 | 104 | # `{{assertion.stringify.gsub(/\(.*\)/, "").id}}` assertion shortcut method. 105 | # 106 | # Can be used for ad hoc validations when applying annotations is not possible. 107 | def self.{{method_name}}({{method_args.splat}}) : Bool forall {{free_variables.splat}} 108 | assertion = {{assertion.name.gsub(/\(.*\)/, "").id}}({{method_args.first.restriction}}{{generic_args}}).new(\{{@def.args.first.name.stringify}}, {{method_vars}}) 109 | assertion.valid? 110 | end 111 | 112 | # `{{assertion.stringify.gsub(/\(.*\)/, "").id}}` assertion shortcut method. 113 | # 114 | # Can be used for ad hoc validations when applying annotations is not possible. 115 | # Raises an `Assert::Exceptions::ValidationError` if the value is not valid. 116 | def self.{{method_name}}!({{method_args.splat}}) : Bool forall {{free_variables.splat}} 117 | assertion = {{assertion.name.gsub(/\(.*\)/, "").id}}({{method_args.first.restriction}}{{generic_args}}).new(\{{@def.args.first.name.stringify}}, {{method_vars}}) 118 | assertion.valid? || raise Assert::Exceptions::ValidationError.new assertion 119 | end 120 | {% end %} 121 | {% end %} 122 | {% end %} 123 | end 124 | 125 | # Returns `true` if `self` is valid, otherwise `false`. 126 | # Optionally only run assertions a part of the provided *groups*. 127 | def valid?(*groups : String) : Bool 128 | valid? groups.to_a 129 | end 130 | 131 | # :ditto: 132 | def valid?(groups : Array(String) = Array(String).new) : Bool 133 | validate(groups).empty? 134 | end 135 | 136 | # Runs the assertions on `self`, returning the assertions that are not valid. 137 | # Optionally only run assertions a part of the provided *groups*. 138 | def validate(*groups : String) : Array(Assert::Assertions::Assertion) 139 | validate groups.to_a 140 | end 141 | 142 | # :ditto: 143 | def validate(groups : Array(String) = Array(String).new) : Array(Assert::Assertions::Assertion) 144 | {% begin %} 145 | {% assertions = [] of String %} 146 | {% for ivar in @type.instance_vars %} 147 | # TODO: Remove once https://github.com/crystal-lang/crystal/issues/8093 is resolved 148 | {% for assertion in Assert::Assertions::Assertion.subclasses.select(&.name.stringify.includes?("(P")) %} 149 | {% if (ann_class = assertion.annotation(Assert::Assertions::Register)) && ann_class[:annotation] != nil && (ann = ivar.annotation(ann_class[:annotation].resolve)) %} 150 | {% raise "#{@type}'s #{ann_class} assertion must not set 'property_name'." if ann["property_name"] %} 151 | {% raise "#{@type}'s #{ann_class} assertion must not set 'actual'." if ann["actual"] %} 152 | {% assertions << %(#{assertion.name.gsub(/\(.*\)/, "")}(#{ivar.type}#{ann.args.empty? ? "".id : ",#{ann.args.splat}".id}).new(property_name: #{ivar.name.stringify}, actual: #{ivar.id}, #{ann.named_args.double_splat})).id %} 153 | {% end %} 154 | {% end %} 155 | {% end %} 156 | assertions = {{assertions}} of Assert::Assertions::Assertion 157 | 158 | assertions.reject! { |a| (a.groups & groups).empty? } unless groups.empty? 159 | 160 | assertions.reject &.valid? 161 | {% end %} 162 | end 163 | 164 | # Runs the assertions on `self`, raises an `Assert::Exceptions::ValidationError` if `self` is not valid. 165 | # 166 | # Optionally only run assertions a part of the provided *groups*. 167 | def validate!(*groups : String) : Nil 168 | validate! groups.to_a 169 | end 170 | 171 | # :ditto: 172 | def validate!(groups : Array(String) = Array(String).new) : Nil 173 | failed_assertions = validate groups 174 | raise Assert::Exceptions::ValidationError.new failed_assertions unless failed_assertions.empty? 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/assert_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | class Test 4 | include Assert 5 | 6 | def initialize(@group_1 : Int32, @group_2 : Int32, @default_group : Int32); end 7 | 8 | @[Assert::EqualTo(value: 100, groups: ["group1"])] 9 | property group_1 : Int32 10 | 11 | @[Assert::EqualTo(value: 200, groups: ["group2"])] 12 | property group_2 : Int32 13 | 14 | @[Assert::EqualTo(value: 300)] 15 | property default_group : Int32 16 | end 17 | 18 | class SomeClass 19 | include Assert 20 | 21 | def initialize; end 22 | 23 | @[Assert::Size(Range(Int32, Int32), range: 5..5)] 24 | property exact_value : String = "he" 25 | end 26 | 27 | describe Assert do 28 | # Will just do a few of each "type" 29 | describe "shortcut methods" do 30 | # Allows any type 31 | describe ".not_nil" do 32 | describe "with a not nil value" do 33 | it "should be true" do 34 | Assert.not_nil(17).should be_true 35 | end 36 | end 37 | 38 | describe "with a nil value" do 39 | it "should be false" do 40 | Assert.not_nil(nil).should be_false 41 | end 42 | end 43 | end 44 | 45 | describe ".not_nil!" do 46 | describe "with a not nil value" do 47 | it "should be true" do 48 | Assert.not_nil!(17).should be_true 49 | end 50 | end 51 | 52 | describe "with a nil value" do 53 | it "should raise an exception" do 54 | expect_raises Assert::Exceptions::ValidationError, "Validation tests failed: 'actual' should not be null" do 55 | Assert.not_nil!(nil) 56 | end 57 | end 58 | end 59 | 60 | describe "with a custom message" do 61 | it "should raise an exception with the given message" do 62 | expect_raises Assert::Exceptions::ValidationError, "Validation tests failed: Age should not be nil" do 63 | Assert.not_nil!(nil, message: "Age should not be nil") 64 | end 65 | end 66 | end 67 | end 68 | 69 | # Allows specific type 70 | describe ".not_blank" do 71 | describe "with a non blank value" do 72 | it "should be true" do 73 | Assert.not_blank("foo").should be_true 74 | end 75 | end 76 | 77 | describe "with a blank value" do 78 | it "should be false" do 79 | Assert.not_blank(" ").should be_false 80 | end 81 | end 82 | 83 | describe "with a normalizer" do 84 | it "should be normalized to be true" do 85 | Assert.not_blank(" ", normalizer: ->(actual : String) { actual + 'f' }).should be_true 86 | end 87 | end 88 | end 89 | 90 | describe ".not_blank!" do 91 | describe "with a non blank value" do 92 | it "should be true" do 93 | Assert.not_blank!("foo").should be_true 94 | end 95 | end 96 | 97 | describe "with a blank value" do 98 | it "should raise an exception" do 99 | expect_raises Assert::Exceptions::ValidationError, "Validation tests failed: 'actual' should not be blank" do 100 | Assert.not_blank!("") 101 | end 102 | end 103 | end 104 | end 105 | 106 | # Has multiple types 107 | describe ".choice" do 108 | describe "with a valid choice" do 109 | it "should be true" do 110 | Assert.choice("Choice", ["One", "Foo", "Choice"]).should be_true 111 | end 112 | end 113 | 114 | describe "with an invalid choice" do 115 | it "should be false" do 116 | Assert.choice("Bar", ["One", "Foo", "Choice"]).should be_false 117 | end 118 | end 119 | end 120 | 121 | describe ".choice!" do 122 | describe "with a valid choice" do 123 | it "should be true" do 124 | Assert.choice!("Choice", ["One", "Foo", "Choice"]).should be_true 125 | end 126 | end 127 | 128 | describe "with an invalid choice" do 129 | it "should raise an exception" do 130 | expect_raises Assert::Exceptions::ValidationError, "Validation tests failed: 'actual' is not a valid choice" do 131 | Assert.choice!("Bar", ["One", "Foo", "Choice"]) 132 | end 133 | end 134 | end 135 | 136 | describe "with a custom message" do 137 | it "should raise an exception with the given message" do 138 | expect_raises Assert::Exceptions::ValidationError, "Validation tests failed: Invalid choice: Bar" do 139 | Assert.choice!("Bar", ["One", "Foo", "Choice"], message: "Invalid choice: %{actual}") 140 | end 141 | end 142 | end 143 | end 144 | end 145 | 146 | describe "#valid?" do 147 | describe "with a valid object" do 148 | it "should return true" do 149 | Test.new(100, 200, 300).valid?.should be_true 150 | end 151 | end 152 | 153 | describe "with an invalid object" do 154 | it "should return false" do 155 | Test.new(100, 100, 300).valid?.should be_false 156 | end 157 | end 158 | 159 | describe :groups do 160 | describe Array do 161 | it "should run assertions in that group" do 162 | Test.new(100, 100, 100).valid?(["group1"]).should be_true 163 | end 164 | end 165 | 166 | describe "splat" do 167 | it "should run assertions in that group" do 168 | Test.new(100, 100, 100).valid?(["group1"]).should be_true 169 | end 170 | end 171 | 172 | describe "default" do 173 | it "assertions without explicit groups should be in the default group" do 174 | Test.new(200, 100, 300).valid?(["default"]).should be_true 175 | end 176 | end 177 | end 178 | end 179 | 180 | describe "#validate" do 181 | it "should preserve the message if it was changed" do 182 | SomeClass.new.validate.first.message.should eq "'exact_value' is not the proper size. It should have exactly 5 character(s)" 183 | end 184 | 185 | describe "with a valid object" do 186 | it "should return an empty array" do 187 | Test.new(100, 200, 300).validate.should be_empty 188 | end 189 | end 190 | 191 | describe "with an invalid object" do 192 | it "should return a non empty array" do 193 | Test.new(100, 100, 100).validate.should_not be_empty 194 | end 195 | end 196 | 197 | describe :groups do 198 | describe Array do 199 | it "should run assertions in that group" do 200 | Test.new(50, 200, 100).validate(["group2"]).should be_empty 201 | end 202 | end 203 | 204 | describe "splat" do 205 | it "should run assertions in that group" do 206 | Test.new(100, 250, 100).validate("group1").should be_empty 207 | end 208 | end 209 | 210 | describe "default" do 211 | it "assertions without explicit groups should be in the default group" do 212 | Test.new(200, 100, 300).validate(["default"]).should be_empty 213 | end 214 | end 215 | end 216 | end 217 | 218 | describe "#validate!" do 219 | it "should preserve the message if it was changed" do 220 | SomeClass.new.validate! 221 | rescue ex : Assert::Exceptions::ValidationError 222 | ex.to_s.should eq "Validation tests failed: 'exact_value' is not the proper size. It should have exactly 5 character(s)" 223 | end 224 | 225 | describe "with a valid object" do 226 | it "should return true" do 227 | Test.new(100, 200, 300).validate!.should be_nil 228 | end 229 | end 230 | 231 | describe "with an invalid object" do 232 | it "should raise the proper exception" do 233 | expect_raises(Assert::Exceptions::ValidationError, "Validation tests failed") do 234 | Test.new(100, 100, 100).validate! 235 | end 236 | end 237 | end 238 | 239 | describe :groups do 240 | describe Array do 241 | it "should run assertions in that group" do 242 | Test.new(50, 200, 150).validate!(["group2"]).should be_nil 243 | end 244 | end 245 | 246 | describe "splat" do 247 | it "should run assertions in that group" do 248 | Test.new(100, 250, 150).validate!("group1").should be_nil 249 | end 250 | end 251 | 252 | describe "default" do 253 | it "assertions without explicit groups should be in the default group" do 254 | Test.new(200, 100, 300).validate!(["default"]).should be_nil 255 | end 256 | end 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /src/assertion.cr: -------------------------------------------------------------------------------- 1 | # See `Assert::Assertions::Assertion` for assertion usage documentation as well as 2 | # `Assert::Assertions` for the full list of assertions. 3 | module Assert::Assertions 4 | # Contains metadata associated with an `Assertion`. 5 | # 6 | # Used to define the assertion's annotation. 7 | # 8 | # ``` 9 | # @[Assert::Assertions::Register(annotation: Assert::MyAssertion)] 10 | # class MyAssertion(PropertyType) < Assert::Assertions::Assertion 11 | # end 12 | # ``` 13 | annotation Register; end 14 | 15 | # Base class of all assertions. 16 | # 17 | # An assertion consists of: 18 | # 1. An annotation applied to a property. 19 | # 2. A class that implements the validation logic for that annotation. 20 | # 21 | # Each assertion defines an *actual* ivar that represents the current value 22 | # of the property. The type of *actual* is used to determine what types of properties 23 | # the annotation can be applied to. I.e. if the type is `String`, that assertion 24 | # can only be applied to `String` properties. The generic `P` can also be used, which 25 | # would allow it to be added to any type of property. 26 | # 27 | # All assertions must inherit from `Assertion` as well as apply the `Register` annotation, and give the name of the annotation; `Assert::SomeAssertion` for example. 28 | # 29 | # Assertions can define additional ivars within the initializer. The name of each ivar 30 | # is also used as the name to use on the annotation. 31 | # 32 | # Ivars with a default value are considered optional. The default value 33 | # will be used if that value is not specified within the annotation. 34 | # 35 | # `Assertion.initializer` can be used as an easier way to define the `initialize` method. 36 | # 37 | # ### Custom Assertions 38 | # Custom assertions can easily be defined by inheriting from `Assertion`, applying the `Register` annotation, and setting the name of the annotation to read. 39 | # 40 | # ``` 41 | # @[Assert::Assertions::Register(annotation: Assert::MyCustom)] 42 | # class MyCustom(PropertyType) < Assert::Assertions::Assertion 43 | # def initialize( 44 | # @actual : String?, # This assertion can only be used on `String?` or `String` properties. Is set automatically. 45 | # @some_value : Int32, # A required argument, must be supplied within the annotation or a compile time error will be raised. 46 | # property_name : String, # The property this assertion was applied to. Is set automatically. 47 | # @some_bool : Bool = true, # An optional argument. The value will be `true` if *some_bool* is not specified within the annotation. 48 | # message : String? = nil, # Optionally override the message on a per property basis. 49 | # groups : Array(String)? = nil # Optionally override the groups on a per property basis. 50 | # ) 51 | # super property_name, message, groups # Be sure to call `super property_name, message, groups` to set the property_name/message/groups. 52 | # end 53 | # 54 | # # The initialize method could also have been written using the `Assertion.initializer` macro. 55 | # initializer( 56 | # actual: String?, 57 | # some_value: Int32, 58 | # some_bool: "Bool = true" 59 | # ) 60 | # 61 | # # :inherit: 62 | # def default_message_template : String 63 | # # The default message template to use if one is not supplied. 64 | # # Instance variables on `self` can be referenced/formatted within the template by using named sprintf named arguments. E.x. `%{some_bool}`, or `%0.4d`, etc. 65 | # "'%{property_name}' is not valid." 66 | # end 67 | # 68 | # # :inherit: 69 | # def valid? : Bool 70 | # # Define validation logic 71 | # end 72 | # end 73 | # ``` 74 | # 75 | # NOTE: Every assertion must have a generic `PropertyType` as its first generic type variable; even if the assertion is not using it. 76 | # 77 | # This custom assertion is now ready to use. Be sure to `include Assert`. 78 | # ``` 79 | # # Since *some_value* does not have a default value, it is required. 80 | # @[Assert::MyCustom(some_value: 123)] 81 | # property name : String 82 | # 83 | # # Override the default value of *some_bool*. 84 | # @[Assert::MyCustom(some_value: 456, some_bool: false)] 85 | # property name : String 86 | # ``` 87 | # 88 | # ### Validation Groups 89 | # By default, `Assert` bases the validity of an object based on _all_ the assertions defined on it. However, each assertion has an optional 90 | # `#groups` property that can be used to assign that assertion to a given group(s). Assertions without explicit groups are automatically assigned 91 | # to the "default" group. This allows assertions without a group to be ran in conjunction with those in an explicit group. 92 | # Validation groups can be used to run a subset of assertions, and base the validity of the object on that subset. 93 | # ``` 94 | # class Groups 95 | # include Assert 96 | # 97 | # def initialize(@group_1 : Int32, @group_2 : Int32, @default_group : Int32); end 98 | # 99 | # @[Assert::EqualTo(value: 100, groups: ["group1"])] 100 | # property group_1 : Int32 101 | # 102 | # @[Assert::EqualTo(value: 200, groups: ["group2"])] 103 | # property group_2 : Int32 104 | # 105 | # @[Assert::EqualTo(value: 300)] 106 | # property default_group : Int32 107 | # end 108 | # 109 | # Groups.new(100, 200, 300).valid? # => true 110 | # Groups.new(100, 100, 100).valid? # => false 111 | # Groups.new(100, 100, 100).valid?(["group1"]) # => true 112 | # Groups.new(200, 100, 300).valid?(["default"]) # => true 113 | # Groups.new(100, 200, 200).valid?("group1", "default") # => false 114 | # ``` 115 | # 116 | # ### Generics 117 | # 118 | # An assertion can utilize additional generic type variables, other than the required `PropertyType`. These would then be provided as positional arguments on the assertion annotation. 119 | # 120 | # ``` 121 | # @[Assert::Assertions::Register(annotation: Assert::Exists)] 122 | # # A custom assertion that validates if a record exists with the given *id*. 123 | # # 124 | # # For example, an ORM model where `.exists?` checks if a record exists with the given PK. 125 | # # I.e. `SELECT exists(select 1 from "users" WHERE id = 123);` 126 | # class Exists(PropertyType, Model) < Assert::Assertions::Assertion 127 | # initializer( 128 | # actual: PropertyType 129 | # ) 130 | # 131 | # # :inherit: 132 | # def default_message_template : String 133 | # "'%{actual}' is not a valid %{property_name}." 134 | # end 135 | # 136 | # # :inherit: 137 | # def valid? : Bool 138 | # # Can use any class method defined on `M` 139 | # Model.exists? @actual 140 | # end 141 | # end 142 | # ``` 143 | # ``` 144 | # class Post < SomeORM::Model 145 | # include Assert 146 | # 147 | # def initialize; end 148 | # 149 | # @[Assert::Exists(User)] 150 | # property author_id : Int64 = 17 151 | # end 152 | # ``` 153 | # This would assert that there is a `User` record with a primary key value of `17`. 154 | # 155 | # Of course you can also define named arguments on the annotation if you wanted to, for example, customize the error message on a per annotation basis. 156 | # ``` 157 | # @[Assert::Exists(User, message: "No user exists with the provided ID")] 158 | # property author_id : Int64 159 | # ``` 160 | # 161 | # ### Extending 162 | # By default, objects must be validated manually; that is using `Assert#valid?`, `Assert#validate`, or `Assert#validate!` on your own. This is left up to the user to allow them to 163 | # control how exactly validation of their objects should work. 164 | # 165 | # `Assert` is easy to integrate into existing frameworks/applications. For example, it could be included in a web framework to automatically run 166 | # assertions on deserialization, by adding some logic to a `after_initialize` method if using `JSON::Serializable`. It could also be added into an ORM to check if the object is valid before saving. 167 | # You could also add logic to an initializer so that it would validate the object on initialization. 168 | # 169 | # NOTE: `nil` is considered to be a valid value if the property is nilable. Either use a non-nilable type, or a `NotNil` assertion. 170 | abstract class Assertion 171 | @message : String? = nil 172 | 173 | # The validation groups `self` is a part of. 174 | getter groups : Array(String) 175 | 176 | # The raw template string that is used to build `#message`. 177 | getter message_template : String 178 | 179 | # The name of the property `self` was applied to. 180 | getter property_name : String 181 | 182 | # Sets the *property_name*, and *message*/*groups* if none were provided. 183 | def initialize(@property_name : String, message : String? = nil, groups : Array(String)? = nil) 184 | @message_template = message || default_message_template 185 | @groups = groups || ["default"] 186 | end 187 | 188 | # Returns the default `#message_template` to use if no *message* is provided. 189 | abstract def default_message_template : String 190 | 191 | # The message to display if `self` is not valid. 192 | # 193 | # NOTE: This method is defined automatically, and is just present for documentation purposes. 194 | abstract def message : String 195 | 196 | # Returns `true` if a property satisfies `self`, otherwise `false`. 197 | abstract def valid? : Bool 198 | 199 | # Builds `initialize` with the provided *ivars*. 200 | # 201 | # Handles setting the required parent arguments and calling super. 202 | # ``` 203 | # initializer(actual: String?, some_bool: "Bool = false") 204 | # # def initialize( 205 | # # property_name : String, 206 | # # @actual : ::Union(String, ::Nil), 207 | # # @some_bool : Bool = false, 208 | # # message : String? = nil, 209 | # # groups : Array(String)? = nil 210 | # # ) 211 | # # super property_name, message, groups 212 | # # end 213 | # ``` 214 | macro initializer(**ivars) 215 | def initialize( 216 | property_name : String, 217 | {% for ivar, type in ivars %} 218 | @{{ivar.id}} : {{type.id}}, 219 | {% end %} 220 | message : String? = nil, 221 | groups : Array(String)? = nil, 222 | ) 223 | super property_name, message, groups 224 | end 225 | end 226 | 227 | macro inherited 228 | # :inherit: 229 | def message : String 230 | @message ||= sprintf(@message_template, 231 | {% verbatim do %} 232 | {% begin %} 233 | { 234 | {% for ivar in @type.instance_vars %} 235 | {{ivar.id}}: @{{ivar}}, 236 | {% end %} 237 | } 238 | {% end %} 239 | {% end %} 240 | ) 241 | end 242 | end 243 | end 244 | end 245 | --------------------------------------------------------------------------------