Skip to content

Commit

Permalink
Merge branch 'feature/validation' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
etki committed Jul 30, 2017
2 parents 6cb2fcb + 6dbb585 commit 405831f
Show file tree
Hide file tree
Showing 25 changed files with 780 additions and 71 deletions.
28 changes: 28 additions & 0 deletions lib/mapper/api/attribute_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# rubocop:disable Lint/UnusedMethodArgument

require_relative 'interface'
require_relative '../mixin/errors'

module AMA
module Entity
class Mapper
module API
# Custom validator for attribute
class AttributeValidator < Interface
include Mixin::Errors
# :nocov:
# @param [Object] value Attribute value
# @param [Mapper::Type::Attribute] attribute
# @param [Mapper::Context] context
# @return [Array<String>] List of violations
def validate(value, attribute, context)
abstract_method
end
# :nocov:
end
end
end
end
end
66 changes: 66 additions & 0 deletions lib/mapper/api/default/attribute_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

require_relative '../attribute_validator'

module AMA
module Entity
class Mapper
module API
module Default
# Default validator for single attribute
class AttributeValidator < API::AttributeValidator
INSTANCE = new

# @param [Object] value Attribute value
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
# @return [Array<String>] Single violation, list of violations
def validate(value, attribute, *)
violations = validate_internal(value, attribute)
violations.nil? ? [] : [violations]
end

private

def validate_internal(value, attribute)
if illegal_nil?(value, attribute)
return "Attribute #{attribute} could not be nil"
end
if invalid_type?(value, attribute)
return "Provided value #{value} doesn't conform to " \
"any of attribute #{attribute} types (#{attribute.types})"
end
return unless illegal_value?(value, attribute)
"Provided value #{value} doesn't match default value (#{value})" \
" or any of allowed values (#{attribute.values})"
end

# @param [Object] value Attribute value
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
# @return [TrueClass, FalseClass]
def illegal_nil?(value, attribute)
value.nil? && !attribute.nullable
end

# @param [Object] value Attribute value
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
# @return [TrueClass, FalseClass]
def invalid_type?(value, attribute)
attribute.types.all? do |type|
!type.respond_to?(:instance?) || !type.instance?(value)
end
end

# @param [Object] value Attribute value
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
# @return [TrueClass, FalseClass]
def illegal_value?(value, attribute)
return false if value == attribute.default
return false if attribute.values.empty? || attribute.values.nil?
!attribute.values.include?(value)
end
end
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/mapper/api/default/entity_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require_relative '../entity_validator'

module AMA
module Entity
class Mapper
module API
module Default
# Default entity validation
class EntityValidator < API::EntityValidator
INSTANCE = new

# @param [Object] entity
# @param [Mapper::Type::Concrete] type
# @param [Mapper::Context] context
# @return [Array<Array<Attribute, String, Segment>] List of
# violations, combined with attribute and segment
def validate(entity, type, context)
enumerator = type.enumerator.enumerate(entity, type, context)
enumerator.flat_map do |attribute, value, segment = nil|
next_context = segment.nil? ? context : context.advance(segment)
validator = attribute.validator
violations = validator.validate(value, attribute, next_context)
violations.map do |violation|
[attribute, violation, segment]
end
end
end
end
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/mapper/api/entity_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

# rubocop:disable Lint/UnusedMethodArgument

require_relative 'interface'

module AMA
module Entity
class Mapper
module API
# Custom validator for entity
class EntityValidator < Interface
# :nocov:
# @param [Object] entity
# @param [Mapper::Type::Concrete] type
# @param [Mapper::Context] context
# @return [Array<Array<Attribute, String, Segment>] List of
# violations, combined with attribute and segment
def validate(entity, type, context)
abstract_method
end
# :nocov:
end
end
end
end
end
41 changes: 41 additions & 0 deletions lib/mapper/api/wrapper/attribute_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require_relative '../attribute_validator'

module AMA
module Entity
class Mapper
module API
module Wrapper
# Attribute validator safety wrapper
class AttributeValidator < API::AttributeValidator
# @param [AMA::Entity::Mapper::API::AttributeValidator] validator
def initialize(validator)
@validator = validator
end

# @param [Object] value Attribute value
# @param [AMA::Entity::Mapper::Type::Attribute] attr
# @param [AMA::Entity::Mapper::Context] ctx
# @return [Array<String>] List of violations
def validate(value, attr, ctx)
violations = @validator.validate(value, attr, ctx) do |v, a, c|
API::Default::AttributeValidator::INSTANCE.validate(v, a, c)
end
violations = [violations] if violations.is_a?(String)
violations.nil? ? [] : violations
rescue StandardError => e
raise_if_internal(e)
message = "Error during #{attr} validation (value: #{value})"
if e.is_a?(ArgumentError)
message += '. Does provided validator have ' \
'(value, attribute, context) signature?'
end
compliance_error(message, context: ctx, parent: e)
end
end
end
end
end
end
end
73 changes: 73 additions & 0 deletions lib/mapper/api/wrapper/entity_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require_relative '../entity_validator'
require_relative '../../type/attribute'

module AMA
module Entity
class Mapper
module API
module Wrapper
# Default entity validation
class EntityValidator < API::EntityValidator
# @param [AMA::Entity::Mapper::API::EntityValidator] validator
def initialize(validator)
@validator = validator
end

# @param [Object] entity
# @param [Mapper::Type::Concrete] type
# @param [Mapper::Context] context
# @return [Array<Array<Attribute, String, Segment>] List of
# violations, combined with attribute and segment
def validate(entity, type, context)
result = @validator.validate(entity, type, context) do |e, t, c|
API::Default::EntityValidator::INSTANCE.validate(e, t, c)
end
verify_result!(result, type, context)
result
rescue StandardError => e
raise_if_internal(e)
message = "Error during #{type} validation (entity: #{entity})"
if e.is_a?(ArgumentError)
message += '. Does provided validator have ' \
'(entity, type, context) signature?'
end
compliance_error(message, context: context, parent: e)
end

private

def verify_result!(result, type, context)
unless result.is_a?(Array)
message = "Validator #{@validator} for type #{type} " \
'had to return list of violations, ' \
"#{result} was received instead"
compliance_error(message, context: context)
end
result.each do |violation|
verify_violation!(violation, type, context)
end
end

def verify_violation!(violation, type, context)
message = "Validator #{@validator} for type #{type} " \
"has returned #{violation} as violation " \
'([attribute, violation, path segment] expected)'
unless violation.is_a?(Array) || violation.size == 3
compliance_error(message, context: context)
end
conditions = [
violation[0].is_a?(Type::Attribute),
violation[1].is_a?(String),
violation[2].is_a?(Path::Segment) || violation[2].nil?
]
return if conditions.all?(&:itself)
compliance_error(message, context: context)
end
end
end
end
end
end
end
5 changes: 4 additions & 1 deletion lib/mapper/dsl/class_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ def bound_type
# @param [Hash] options Attribute options: :virtual, :sensitive
# @return [AMA::Entity::Mapper::Type::Attribute]
def attribute(name, *types, **options)
types = types.map { |type| @mapper.resolve(type) }
types = types.map do |type|
next parameter(type) if type.is_a?(Symbol) || type.is_a?(String)
@mapper.resolve(type)
end
bound_type.attribute!(name, *types, **options)
define_method(name) do
instance_variable_get("@#{name}")
Expand Down
Loading

0 comments on commit 405831f

Please sign in to comment.