Skip to content

Commit

Permalink
Implemented resolver class, inherited Any from Concrete type, refurbi…
Browse files Browse the repository at this point in the history
…shed some tests
  • Loading branch information
etki committed Jul 27, 2017
1 parent 8858bae commit da9d362
Show file tree
Hide file tree
Showing 14 changed files with 514 additions and 188 deletions.
20 changes: 4 additions & 16 deletions lib/mapper/engine.rb
Expand Up @@ -4,6 +4,7 @@
require_relative 'context'
require_relative 'mixin/errors'
require_relative 'type/registry'
require_relative 'type/resolver'
require_relative 'type/concrete'
require_relative 'engine/normalizer'
require_relative 'engine/denormalizer'
Expand All @@ -17,9 +18,11 @@ class Engine
include Mixin::Errors

attr_reader :registry
attr_reader :resolver

def initialize(registry = nil)
@registry = registry || Type::Registry.new
@resolver = Type::Resolver.new(@registry)
@normalizer = Normalizer.new
@denormalizer = Denormalizer.new
end
Expand Down Expand Up @@ -113,27 +116,12 @@ def normalize_types(types, context)
compliance_error('Requested map operation with no target types')
end
types = types.map do |type|
normalize_type(type)
@resolver.resolve(type)
end
types.each do |type|
type.resolved!(context)
end
end

def normalize_type(type)
return type if type.is_a?(Type)
parameters = {}
type, parameters = type if type.is_a?(Array)
if [Module, Class].any? { |candidate| type.is_a?(candidate) }
type = @registry[type] || Type::Concrete.new(type)
end
unless type.is_a?(Type)
message = "Provided type in unknown format: #{type}, " \
'Type/Class/Module expected'
compliance_error(message)
end
type.resolve(parameters)
end
end
end
end
Expand Down
3 changes: 0 additions & 3 deletions lib/mapper/type.rb
Expand Up @@ -79,9 +79,6 @@ def resolved?
# @param [AMA::Entity::Mapper::Context] context
def resolved!(context = nil)
context ||= Context.new
unless parameters.values.reject(&:resolved?).empty?
compliance_error("Type #{self} is not resolved", context: context)
end
attributes.values.each do |attribute|
attribute.resolved!(context)
end
Expand Down
11 changes: 10 additions & 1 deletion lib/mapper/type/any.rb
@@ -1,19 +1,28 @@
# frozen_string_literal: true

require_relative 'concrete'
require_relative '../mixin/errors'

module AMA
module Entity
class Mapper
class Type
# Used as a wildcard to pass anything through
class Any < Type
class Any < Concrete
include Mixin::Errors

def initialize; end

INSTANCE = new

def parameters
{}
end

def attributes
{}
end

def parameter!(*)
compliance_error('Tried to declare parameter on Any type')
end
Expand Down
10 changes: 7 additions & 3 deletions lib/mapper/type/attribute.rb
Expand Up @@ -56,9 +56,13 @@ def resolved!(context = nil)
# @return [AMA::Entity::Mapper::Type::Attribute]
def resolve_parameter(parameter, substitution)
clone.tap do |clone|
clone.types = types.map do |type|
next substitution if type == parameter
type.resolve_parameter(parameter, substitution)
clone.types = types.each_with_object([]) do |type, carrier|
if type == parameter
buffer = substitution
buffer = [buffer] unless buffer.is_a?(Enumerable)
next carrier.push(*buffer)
end
carrier.push(type.resolve_parameter(parameter, substitution))
end
end
end
Expand Down
56 changes: 35 additions & 21 deletions lib/mapper/type/concrete.rb
Expand Up @@ -119,20 +119,21 @@ def attribute!(name, *types, **options)
# Creates new type parameter
#
# @param [Symbol] id
# @return [AMA::Entity::Mapper::Type::Parameter]
# @return [Parameter]
def parameter!(id)
id = id.to_sym
return parameters[id] if parameters.key?(id)
parameters[id] = Parameter.new(self, id)
end

# Resolves single parameter type
# Resolves single parameter type. Substitution may be either another
# parameter, concrete type or array of concrete types.
#
# @param [AMA::Entity::Mapper::Type::Parameter] parameter
# @param [AMA::Entity::Mapper::Type] substitution
def resolve_parameter(parameter, substitution, context = nil)
parameter = normalize_parameter(parameter, context)
substitution = normalize_substitution(substitution, context)
# @param [Parameter] parameter
# @param [Parameter, Array<Concrete>] substitution
def resolve_parameter(parameter, substitution)
parameter = validate_parameter!(parameter)
substitution = validate_substitution!(substitution)
clone.tap do |clone|
intermediate = attributes.map do |key, value|
[key, value.resolve_parameter(parameter, substitution)]
Expand Down Expand Up @@ -166,18 +167,21 @@ def injector_block(&block)
end

def hash
@type.hash
@type.hash ^ @attributes.hash
end

def eql?(other)
return false unless other.is_a?(self.class)
@type == other.type
@type == other.type && @attributes == other.attributes
end

def to_s
representation = @type.to_s
return representation if parameters.empty?
params = parameters.map do |key, value|
if value.is_a?(Enumerable)
value = "[#{value.map(&:to_s).join(', ')}]"
end
value = value.is_a?(Parameter) ? '?' : value.to_s
"#{key}:#{value}"
end
Expand All @@ -193,24 +197,34 @@ def validate_type!(type)
compliance_error(message)
end

def normalize_parameter(parameter, context = nil)
if parameter.is_a?(Symbol) && parameters.key?(parameter)
return parameters[parameter]
end
def validate_parameter!(parameter)
return parameter if parameter.is_a?(Parameter)
message = "Non-parameter type #{parameter} " \
'supplied for resolution'
compliance_error(message, context: context)
compliance_error(message)
end

def validate_substitution!(substitution)
return substitution if substitution.is_a?(Parameter)
substitution = [substitution] if substitution.is_a?(self.class)
if substitution.is_a?(Enumerable)
return validate_substitutions!(substitution)
end
message = 'Provided substitution is neither another Parameter ' \
'or Array of concrete Types: ' \
"#{substitution} (#{substitution.class})"
compliance_error(message)
end

def normalize_substitution(substitution, context)
return substitution if substitution.is_a?(Type)
if [Module, Class].any? { |type| substitution.is_a?(type) }
return Concrete.new(substitution)
def validate_substitutions!(substitutions)
if substitutions.empty?
compliance_error('Empty list of substitutions passed')
end
invalid = substitutions.reject do |substitution|
substitution.is_a?(Concrete)
end
message = "#{substitution.class} is passed as parameter " \
'substitution, Type / Class / Module expected'
compliance_error(message, context: context)
return substitutions if invalid.empty?
compliance_error("Invalid substitutions supplied: #{invalid}")
end
end
end
Expand Down
1 change: 0 additions & 1 deletion lib/mapper/type/parameter.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require_relative 'any'
require_relative '../type'
require_relative '../mixin/errors'

Expand Down
118 changes: 50 additions & 68 deletions lib/mapper/type/registry.rb
Expand Up @@ -23,6 +23,29 @@ def initialize
@types = {}
end

# @return [AMA::Entity::Mapper::Type::Registry]
def with_default_types
register(Hardwired::EnumerableType::INSTANCE)
register(Hardwired::HashType::INSTANCE)
register(Hardwired::SetType::INSTANCE)
register(Hardwired::PairType::INSTANCE)
Hardwired::PrimitiveType::ALL.each do |type|
register(type)
end
self
end

# @param [Class, Module] klass
def [](klass)
@types[klass]
end

# @param [AMA::Entity::Mapper::Type] type
# @param [Class, Module] klass
def []=(klass, type)
@types[klass] = type
end

# @param [AMA::Entity::Mapper::Type::Concrete] type
def register(type)
@types[type.type] = type
Expand All @@ -35,99 +58,58 @@ def key?(klass)

alias registered? key?

def applicable(klass)
find_class_types(klass) | find_module_types(klass)
# @param [Class, Module] klass
# @return [Array<AMA::Entity::Mapper::Type>]
def select(klass)
types = class_hierarchy(klass).map do |entry|
@types[entry]
end
types.reject(&:nil?)
end

# @param [Class, Module] klass
# @return [AMA::Entity::Mapper::Type, NilClass]
def find(klass)
candidates = applicable(klass)
candidates = select(klass)
candidates.empty? ? nil : candidates.first
end

# @param [Class, Module] klass
# @return [AMA::Entity::Mapper::Type]
def find!(klass)
candidate = find(klass)
return candidate if candidate
message = "Could not find any registered type for class #{klass}"
compliance_error(message)
end

# @param [Class, Module] klass
# @return [TrueClass, FalseClass]
def include?(klass)
!find(klass).nil?
end

def [](klass)
@types[klass]
end

def resolve(definition)
if definition.is_a?(Module) || definition.is_a?(Class)
definition = [definition]
end
klass, parameters = definition
parameters ||= {}
type = @types[klass] || Concrete.new(klass)
parameters.each do |parameter, replacement|
validate_replacement!(replacement)
parameter = resolve_type_parameter(type, parameter)
type = type.resolve_parameter(parameter, replacement)
end
type
end

def with_default_types
register(Hardwired::EnumerableType::INSTANCE)
register(Hardwired::HashType::INSTANCE)
register(Hardwired::SetType::INSTANCE)
register(Hardwired::PairType::INSTANCE)
Hardwired::PrimitiveType::ALL.each do |type|
register(type)
end
self
!select(klass).empty?
end

private

def validate_replacement!(replacement, context = nil)
return if replacement.is_a?(Type)
message = 'Invalid parameter replacement supplied, expected Type ' \
"instance, got #{replacement} (#{replacement.class})"
compliance_error(message, context: context)
end

def resolve_type_parameter(type, parameter)
return parameter if parameter.is_a?(Parameter)
if parameter.is_a?(Symbol) && type.parameter?(parameter)
return type.parameters[parameter]
end
compliance_error("#{type} has no parameter #{parameter}")
end

def inheritance_chain(klass)
cursor = klass
# @param [Class, Module] klass
def class_hierarchy(klass)
ptr = klass
chain = []
loop do
chain.push(cursor)
cursor = cursor.superclass
break if cursor.nil?
chain.push(*class_with_modules(ptr))
break if !ptr.respond_to?(:superclass) || ptr.superclass.nil?
ptr = ptr.superclass
end
chain
end

def find_class_types(klass)
inheritance_chain(klass).each_with_object([]) do |entry, carrier|
carrier.push(types[entry]) if types[entry]
end
end

def find_module_types(klass)
chain = inheritance_chain(klass).reverse
result = chain.reduce([]) do |carrier, entry|
ancestor_types = entry.ancestors.map do |candidate|
types[candidate]
end
carrier | ancestor_types.reject(&:nil?)
def class_with_modules(klass)
if klass.superclass.nil?
parent_modules = []
else
parent_modules = klass.superclass.included_modules
end
result.reverse
[klass, *(klass.included_modules - parent_modules)]
end
end
end
Expand Down

0 comments on commit da9d362

Please sign in to comment.