Skip to content

Commit

Permalink
Embedded validator spec improvements (#155)
Browse files Browse the repository at this point in the history
* Remove reliance on ivar validator within `ISIN`
* Make `MockValidator` more robust
* Enable some additional `AtLeastOneOf` specs
  • Loading branch information
Blacksmoke16 authored Feb 27, 2022
1 parent 65a249f commit 283f3ce
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ struct AtLeastOneOfValidatorTest < AVD::Spec::ConstraintValidatorTestCase

@[DataProvider("valid_combinations")]
def test_valid_combinations(value : _, constraints : Array(AVD::Constraint)) : Nil
self.validator.validate value, self.new_constraint(constraints: constraints)
constraints.each_with_index do |constraint, idx|
self.expect_violation_at idx, value, constraint
end

self.validator.validate value, self.new_constraint constraints: constraints
self.assert_no_violation
end

Expand All @@ -26,11 +30,33 @@ struct AtLeastOneOfValidatorTest < AVD::Spec::ConstraintValidatorTestCase
end

@[DataProvider("invalid_combinations")]
def ptest_invalid_combinations(value : _, constraints : Array(AVD::Constraint)) : Nil
constraint = self.new_constraint(constraints: constraints)
def test_invalid_combinations_default_message(value : _, constraints : Array(AVD::Constraint)) : Nil
constraint = self.new_constraint constraints: constraints

message = [constraint.message]

constraints.each_with_index do |c, idx|
message << " [#{idx + 1}] #{self.expect_violation_at(idx, value, c).first.message}"
end

self.validator.validate value, constraint

# TODO: Determine how to test this given it depends on an actual validator instance
self
.build_violation(message.join, CONSTRAINT::AT_LEAST_ONE_OF_ERROR)
.assert_violation
end

@[DataProvider("invalid_combinations")]
def test_invalid_combinations_custom_message(value : _, constraints : Array(AVD::Constraint)) : Nil
constraints.each_with_index do |constraint, idx|
self.expect_violation_at idx, value, constraint
end

self.validator.validate value, self.new_constraint constraints: constraints, message: "my_message", include_internal_messages: false

self
.build_violation("my_message", CONSTRAINT::AT_LEAST_ONE_OF_ERROR)
.assert_violation
end

def create_validator : AVD::ConstraintValidatorInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ struct ISINValidatorTest < AVD::Spec::ConstraintValidatorTestCase
end

@[DataProvider("valid_isins")]
def test_valid_isbn10s(value : String) : Nil
def test_valid_isins(value : String) : Nil
self.validator.validate value, self.new_constraint
self.expect_violation_at 0, value, AVD::Constraints::Luhn.new
self.assert_no_violation
end

Expand Down Expand Up @@ -70,6 +71,7 @@ struct ISINValidatorTest < AVD::Spec::ConstraintValidatorTestCase

@[DataProvider("invalid_checksum_isins")]
def test_invalid_checksum_isins(value : String) : Nil
self.expect_violation_at 0, value, AVD::Constraints::Luhn.new
self.assert_violation value, CONSTRAINT::INVALID_CHECKSUM_ERROR
end

Expand Down
4 changes: 1 addition & 3 deletions src/components/validator/src/constraints/isin.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ class Athena::Validator::Constraints::ISIN < Athena::Validator::Constraint
end

struct Validator < Athena::Validator::ConstraintValidator
@validator : AVD::Validator::ValidatorInterface = AVD.validator

# :inherit:
def validate(value : _, constraint : AVD::Constraints::ISIN) : Nil
value = value.to_s
Expand All @@ -81,7 +79,7 @@ class Athena::Validator::Constraints::ISIN < Athena::Validator::Constraint

private def is_correct_checksum(isin : String) : Bool
number = isin.chars.join &.to_i 36
@validator.validate(number, AVD::Constraints::Luhn.new).empty?
self.context.validator.validate(number, AVD::Constraints::Luhn.new).empty?
end
end
end
26 changes: 15 additions & 11 deletions src/components/validator/src/spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ module Athena::Validator::Spec

# A spec implementation of `AVD::Validator::ContextualValidatorInterface`.
#
# Allows settings the violations that should be returned.
# Defaults to no violations.
# Allows settings the violations that should be returned, defaulting to no violations.
class MockContextualValidator
include Athena::Validator::Validator::ContextualValidatorInterface

Expand Down Expand Up @@ -135,38 +134,43 @@ module Athena::Validator::Spec

# A spec implementation of `AVD::Validator::ValidatorInterface`.
#
# Allows settings the violations that should be returned.
# Defaults to no violations.
# Allows settings the violations that should be returned, defaulting to no violations.
# Also allows providing a block that is called for each validated value.
# E.g. to allow dynamically configuring the returned violations after it is instantiated.
class MockValidator
include Athena::Validator::Validator::ValidatorInterface

setter violations : AVD::Violation::ConstraintViolationListInterface
setter violations_callback : Proc(AVD::Violation::ConstraintViolationListInterface)

def initialize(@violations : AVD::Violation::ConstraintViolationListInterface = AVD::Violation::ConstraintViolationList.new); end
def self.new(violations : AVD::Violation::ConstraintViolationListInterface = AVD::Violation::ConstraintViolationList.new) : self
new ->{ violations }
end

def initialize(&@violations_callback : -> AVD::Violation::ConstraintViolationListInterface); end

# :inherit:
def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface
@violations
@violations_callback.call
end

# :inherit:
def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface
@violations
@violations_callback.call
end

# :inherit:
def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface
@violations
@violations_callback.call
end

# :inherit:
def start_context(root = nil) : AVD::Validator::ContextualValidatorInterface
MockContextualValidator.new @violations
MockContextualValidator.new @violations_callback.call
end

# :inherit:
def in_context(context : AVD::ExecutionContextInterface) : AVD::Validator::ContextualValidatorInterface
MockContextualValidator.new @violations
MockContextualValidator.new @violations_callback.call
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ abstract struct Athena::Validator::Spec::ConstraintValidatorTestCase < ASPEC::Te
@constraint : AVD::Constraint
@context : AVD::ExecutionContext?
@validator : AVD::ConstraintValidatorInterface?
@expected_violations : Array(AVD::Violation::ConstraintViolationListInterface)
@call : Int32

protected def initialize
@group = "my_group"
Expand All @@ -157,6 +159,8 @@ abstract struct Athena::Validator::Spec::ConstraintValidatorTestCase < ASPEC::Te
@property_path = "property.path"

@constraint = AVD::Constraints::NotBlank.new
@expected_violations = Array(AVD::Violation::ConstraintViolationListInterface).new
@call = 0

ctx = self.create_context
validator = self.create_validator
Expand Down Expand Up @@ -214,6 +218,24 @@ abstract struct Athena::Validator::Spec::ConstraintValidatorTestCase < ASPEC::Te
self.build_violation(message).code(code).add_parameter("{{ value }}", value)
end

# Can be used to have a nested validator return the correct violations when used within another validator.
#
# Creates a separate validation context, validating the provided *value* against the provided *constraint*,
# causing the resulting violations to be returned from the inner validator as they would be in a non-test context.
#
# See `AVD::Constraints::ISIN::Validator`, and its related specs, for an example.
def expect_violation_at(idx : Int, value : _, constraint : AVD::Constraint) : AVD::Violation::ConstraintViolationListInterface
ctx = self.create_context

validator = constraint.validated_by.new
validator.context = ctx
validator.validate value, constraint

@expected_violations << ctx.violations

ctx.violations
end

# Overrides the value/node currently being validated.
def value=(value) : Nil
@value = value
Expand All @@ -231,7 +253,9 @@ abstract struct Athena::Validator::Spec::ConstraintValidatorTestCase < ASPEC::Te
end

private def create_context : AVD::ExecutionContext
validator = MockValidator.new
validator = MockValidator.new do
(@expected_violations[@call]? || AVD::Violation::ConstraintViolationList.new).tap { @call += 1 }
end

ctx = AVD::ExecutionContext.new validator, @root
ctx.group = @group
Expand Down

0 comments on commit 283f3ce

Please sign in to comment.