diff --git a/lib/mapper/engine.rb b/lib/mapper/engine.rb index cf09f6a..7fa5a8a 100644 --- a/lib/mapper/engine.rb +++ b/lib/mapper/engine.rb @@ -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' @@ -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 @@ -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 diff --git a/lib/mapper/type.rb b/lib/mapper/type.rb index cd59fba..55c1a47 100644 --- a/lib/mapper/type.rb +++ b/lib/mapper/type.rb @@ -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 diff --git a/lib/mapper/type/any.rb b/lib/mapper/type/any.rb index 0a5e881..5da3919 100644 --- a/lib/mapper/type/any.rb +++ b/lib/mapper/type/any.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'concrete' require_relative '../mixin/errors' module AMA @@ -7,13 +8,21 @@ 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 diff --git a/lib/mapper/type/attribute.rb b/lib/mapper/type/attribute.rb index 61cff49..ab779c4 100644 --- a/lib/mapper/type/attribute.rb +++ b/lib/mapper/type/attribute.rb @@ -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 diff --git a/lib/mapper/type/concrete.rb b/lib/mapper/type/concrete.rb index a0dfc10..9173ced 100644 --- a/lib/mapper/type/concrete.rb +++ b/lib/mapper/type/concrete.rb @@ -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] 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)] @@ -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 @@ -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 diff --git a/lib/mapper/type/parameter.rb b/lib/mapper/type/parameter.rb index c0e70d5..1923b53 100644 --- a/lib/mapper/type/parameter.rb +++ b/lib/mapper/type/parameter.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative 'any' require_relative '../type' require_relative '../mixin/errors' diff --git a/lib/mapper/type/registry.rb b/lib/mapper/type/registry.rb index f035b42..6d7b4ab 100644 --- a/lib/mapper/type/registry.rb +++ b/lib/mapper/type/registry.rb @@ -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 @@ -35,15 +58,24 @@ def key?(klass) alias registered? key? - def applicable(klass) - find_class_types(klass) | find_module_types(klass) + # @param [Class, Module] klass + # @return [Array] + 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 @@ -51,83 +83,33 @@ def find!(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 diff --git a/lib/mapper/type/resolver.rb b/lib/mapper/type/resolver.rb new file mode 100644 index 0000000..bc535aa --- /dev/null +++ b/lib/mapper/type/resolver.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative '../mixin/errors' +require_relative 'parameter' +require_relative 'concrete' + +module AMA + module Entity + class Mapper + class Type + # This class is responsible for resolution of simple type definitions, + # converting definitions like + # [Array, T: [NilClass, [Hash, K: Symbol, V: Integer]]] + # into real type hierarchy + class Resolver + include Mixin::Errors + + # @param [Registry] registry + def initialize(registry) + @registry = registry + end + + def resolve(definition) + definition = [definition] unless definition.is_a?(Enumerable) + resolve_definition(definition) + rescue StandardError => parent + message = "Definition #{definition} resolution resulted " \ + "in error: #{parent}" + compliance_error(message) + end + + private + + def resolve_definitions(definitions) + definitions = [definitions] unless definitions.is_a?(Array) + if definitions.size == 2 && definitions.last.is_a?(Hash) + definitions = [definitions] + end + definitions.map do |definition| + resolve_definition(definition) + end + end + + def resolve_definition(definition) + definition = [definition] unless definition.is_a?(Array) + type = definition.first + parameters = definition[1] || {} + resolve_type(type, parameters) + rescue StandardError => e + message = "Unexpected error during definition #{definition} " \ + "resolution: #{e.message}" + compliance_error(message) + end + + def resolve_type(type, parameters) + type = find_type(type) + unless parameters.is_a?(Hash) + message = "Type parameters were passed not as hash: #{parameters}" + compliance_error(message) + end + parameters.each do |parameter, replacements| + parameter = resolve_type_parameter(type, parameter) + replacements = resolve_definitions(replacements) + type = type.resolve_parameter(parameter, replacements) + end + type + end + + def find_type(type) + return type if type.is_a?(Concrete) + if type.is_a?(Class) || type.is_a?(Module) + return @registry[type] || Concrete.new(type) + end + message = 'Invalid type provided for resolution, expected Type, ' \ + "Class or Module: #{type}" + compliance_error(message) + end + + def resolve_type_parameter(type, parameter) + unless parameter.is_a?(Parameter) + parameter = find_parameter(type, parameter) + end + return parameter if parameter.owner.type == type.type + message = "Parameter #{parameter} belongs to different type " \ + 'rather one it is resolved against' + compliance_error(message) + end + + def find_parameter(type, parameter) + parameter = parameter.to_sym if parameter.respond_to?(:to_sym) + unless parameter.is_a?(Symbol) + message = "#{parameter} is not a valid parameter identifier " \ + '(Symbol expected)' + compliance_error(message) + end + return type.parameters[parameter] if type.parameter?(parameter) + message = "Type #{type} has no requested parameter #{parameter}" + compliance_error(message) + end + end + end + end + end +end diff --git a/test/suite/acceptance/mapper/engine.spec.rb b/test/suite/acceptance/mapper/engine.spec.rb index 2588ed1..ea04f12 100644 --- a/test/suite/acceptance/mapper/engine.spec.rb +++ b/test/suite/acceptance/mapper/engine.spec.rb @@ -107,18 +107,18 @@ def self.to_s it 'should denormalize parametrized entity' do source = { value: 12 } type = parametrized_entity - derived = type.resolve(type.parameter!(:T) => type_class.new(Integer)) - result = engine.map(source, derived) - expect(result).to be_a(derived.type) + definition = [parametrized_entity, T: Integer] + result = engine.map(source, definition) + expect(result).to be_a(type.type) expect(result.value).to eq(12) end it 'should denormalize nested entity' do source = { value: { id: :bill, number: 12 } } type = parametrized_entity - derived = type.resolve(type.parameter!(:T) => entity) - result = engine.map(source, derived) - expect(result).to be_a(derived.type) + definition = [parametrized_entity.type, T: entity] + result = engine.map(source, definition) + expect(result).to be_a(type.type) expect(result.value).to be_a(entity.type) expect(result.value.id).to eq(source[:value][:id]) expect(result.value.number).to eq(source[:value][:number]) @@ -129,7 +129,7 @@ def self.to_s 'bill' => { id: :bill, number: 12 }, 'francis' => { id: :francis, number: 13 } } - type = hash_type.resolve(K: registry[Symbol], V: entity) + type = [Hash, K: Symbol, V: entity] result = engine.map(source, type) expect(result).to be_a(Hash) source.each do |key, value| @@ -143,8 +143,7 @@ def self.to_s it 'should denormalize array of entities' do source = [{ id: :bill, number: 12 }, { id: :francis, number: 13 }] - type = enumerable_type.resolve(T: entity) - result = engine.map(source, type) + result = engine.map(source, [Enumerable, T: entity]) expect(result).to be_a(Array) result.each_with_index do |entry, index| value = source[index] @@ -155,11 +154,10 @@ def self.to_s end it 'should denormalize set' do - source = [1, 1, 2, 2, 3, 3, 4, 4] - type = set_type.resolve(T: any_type) - result = engine.map(source, type) + source = [1, 1, 2, 2, 3, 3, 4, 4, :alpha, 'beta'] + result = engine.map(source, [Set, T: any_type]) expect(result).to be_a(Set) - expect(result).to eq(Set.new([1, 2, 3, 4])) + expect(result).to eq(Set.new([1, 2, 3, 4, :alpha, 'beta'])) end it 'should raise compliance error if unresolved type is passed' do @@ -181,8 +179,8 @@ def self.to_s it 'should try next type in case of failure' do source = { id: :bill, number: 12 } types = [ - set_type.resolve(T: registry[Integer]), - enumerable_type.resolve(T: registry[Integer]), + [Set, T: Integer], + [Enumerable, T: Integer], entity ] result = engine.map(source, *types) diff --git a/test/suite/integration/mapper/type/concrete.spec.rb b/test/suite/integration/mapper/type/concrete.spec.rb index 7b8f79c..e7da3d9 100644 --- a/test/suite/integration/mapper/type/concrete.spec.rb +++ b/test/suite/integration/mapper/type/concrete.spec.rb @@ -20,20 +20,36 @@ def self.to_s end let(:parametrized) do - klass.new(Class.new).tap do |instance| + type = Class.new do + def self.to_s + 'Parametrized' + end + end + klass.new(type).tap do |instance| instance.attribute!(:id, Symbol) instance.attribute!(:value, :T) end end let(:resolved) do - klass.new(dummy_class).tap do |instance| + type = Class.new do + attr_accessor :id + def self.to_s + 'Resolved' + end + end + klass.new(type).tap do |instance| instance.attribute!(:id, Symbol) end end let(:nested) do - klass.new(Class.new).tap do |instance| + type = Class.new do + def self.to_s + 'Nested' + end + end + klass.new(type).tap do |instance| instance.attribute!(:id, Symbol) parameter = instance.parameter!(:T) attribute = instance.attribute!(:value, parametrized) @@ -46,55 +62,62 @@ def self.to_s it 'should create new resolved type on call' do type = parametrized expect(type.resolved?).to be false - derivation = type.resolve(type.parameter!(:T) => resolved) + derivation = type.resolve(type.parameter!(:T) => [resolved]) expect(derivation.resolved?).to be true end it 'should recursively resolve types' do type = nested expect(type.resolved?).to be false - derivation = type.resolve(type.parameter!(:T) => resolved) + derivation = type.resolve(type.parameter!(:T) => [resolved]) expect(derivation.resolved?).to be true end end describe '#resolve_parameter' do - it 'should raise compliance error if non-parameter has been passed' do - proc = lambda do - parametrized.resolve_parameter(double, resolved) - end - expect(&proc).to raise_error(compliance_error_class) + it 'substitutes parameter with another parameter' do + substitution = resolved.parameter!(:E) + parameter = parametrized.parameters[:T] + type = parametrized.resolve_parameter(parameter, substitution) + expect(type.parameters[:T]).to eq(substitution) + expect(type.attributes[:value].types).to include(substitution) end - it 'should correctly resolve symbols to parameters' do - type = parametrized.resolve_parameter(:T, resolved) - expect(type.attributes[:value].types).to eq([resolved]) + it 'substitutes parameter with array of types' do + substitution = [resolved] + parameter = parametrized.parameters[:T] + type = parametrized.resolve_parameter(parameter, substitution) + expect(type.parameters[:T]).to eq(substitution) + expect(type.attributes[:value].types).to eq(substitution) end - it 'should raise compliance error if non-existing parameter is specified' do + it 'substitutes parameter with another type' do + substitution = resolved + parameter = parametrized.parameters[:T] + type = parametrized.resolve_parameter(parameter, substitution) + expect(type.parameters[:T]).to eq([substitution]) + expect(type.attributes[:value].types).to include(substitution) + end + + it 'raises compliance error if invalid parameter has been passed' do proc = lambda do - parametrized.resolve_parameter(:Y, Class.new) + parametrized.resolve_parameter(double, resolved) end expect(&proc).to raise_error(compliance_error_class) end - it 'should create concrete type if class is passed as substitution' do - classee = Class.new - parameter = parametrized.parameter!(:T) - resolved = parametrized.resolve_parameter(parameter, classee) - expect(resolved.attributes[:value].types).to eq([klass.new(classee)]) - end - - it 'should create concrete type if module is passed as substitution' do - modulee = Module.new - parameter = parametrized.parameter!(:T) - resolved = parametrized.resolve_parameter(parameter, modulee) - expect(resolved.attributes[:value].types).to eq([klass.new(modulee)]) + it 'raises compliance error if invalid substitution has been passed' do + proc = lambda do + parametrized.resolve_parameter(parametrized.parameters[:T], double) + end + expect(&proc).to raise_error(compliance_error_class) end - it 'should raise compliance error if invalid substitution is provided' do + it 'raises compliance error if array of parameters has been passed' do + substitution = [resolved.parameter!(:E)] + parameter = parametrized.parameters[:T] proc = lambda do - parametrized.resolve_parameter(parametrized.parameter!(:T), 12) + parametrized.resolve_parameter(parameter, substitution) end expect(&proc).to raise_error(compliance_error_class) end @@ -142,7 +165,7 @@ def self.to_s it 'provides default denormalizer' do data = { id: :identifier } result = resolved.denormalizer.denormalize(data, resolved) - expect(result).to be_a(dummy_class) + expect(result).to be_a(resolved.type) expect(result.id).to eq(data[:id]) end end @@ -155,9 +178,9 @@ def self.to_s it 'displays parameters in angle brackets' do type = klass.new(dummy_class) type.parameter!(:A) - type.parameters[:B] = klass.new(dummy_class) + type.parameters[:B] = [klass.new(dummy_class)] dummy = dummy_class.to_s - expect(type.to_s).to eq("#{dummy}") + expect(type.to_s).to eq("#{dummy}") end end diff --git a/test/suite/integration/mapper/type/registry.spec.rb b/test/suite/integration/mapper/type/registry.spec.rb index 0632d3f..69df7c8 100644 --- a/test/suite/integration/mapper/type/registry.spec.rb +++ b/test/suite/integration/mapper/type/registry.spec.rb @@ -6,7 +6,7 @@ klass = ::AMA::Entity::Mapper::Type::Registry type_klass = ::AMA::Entity::Mapper::Type::Concrete -error_klass = ::AMA::Entity::Mapper::Exception::ComplianceError +compliance_error_klass = ::AMA::Entity::Mapper::Exception::ComplianceError describe klass do let(:top) do @@ -65,31 +65,47 @@ def self.to_s Class.new(bottom) end + let(:parametrized_a) do + type_klass.new(Class.new).tap do |type| + type.attribute!(:value, type.parameter!(:T)) + end + end + + let(:parametrized_b) do + type_klass.new(Class.new).tap do |type| + type.attribute!(:value, type.parameter!(:T)) + end + end + let(:registry) do klass.new.tap do |registry| %i[top middle_module middle bottom_module bottom sidecar].each do |type| registry.register(type_klass.new(send(type))) end + %i[a b].each do |suffix| + type = "parametrized_#{suffix}" + registry.register(send(type)) + end end end - describe '#for' do - it 'should return all types in ascending order for bottom class' do - types = registry.applicable(bottom) + describe '#select' do + it 'returns all types in ascending order for bottom class' do + types = registry.select(bottom) classes = types.map(&:type) - expectation = [bottom, middle, top, bottom_module, middle_module] + expectation = [bottom, bottom_module, middle, middle_module, top] expect(classes).to eq(expectation) end - it 'should return top and middle types for middle class' do - types = registry.applicable(middle) + it 'returns top and middle types for middle class' do + types = registry.select(middle) classes = types.map(&:type) - expectation = [middle, top, middle_module] + expectation = [middle, middle_module, top] expect(classes).to eq(expectation) end - it 'should return top type only for top class' do - expect(registry.applicable(top).map(&:type)).to eq([top]) + it 'returns top type only for top class' do + expect(registry.select(top).map(&:type)).to eq([top]) end end @@ -112,8 +128,11 @@ def self.to_s expect(result.type).to eq(bottom) end - it 'should throw on unregistered type' do - expect { registry.find!(Class.new) }.to raise_error(error_klass) + it 'raises on unregistered type' do + proc = lambda do + registry.find!(Class.new) + end + expect(&proc).to raise_error(compliance_error_klass) end end diff --git a/test/suite/integration/mapper/type/resolver.spec.rb b/test/suite/integration/mapper/type/resolver.spec.rb new file mode 100644 index 0000000..7b5e16f --- /dev/null +++ b/test/suite/integration/mapper/type/resolver.spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require_relative '../../../../../lib/mapper/type/resolver' +require_relative '../../../../../lib/mapper/type/concrete' +require_relative '../../../../../lib/mapper/type/registry' +require_relative '../../../../../lib/mapper/type/any' +require_relative '../../../../../lib/mapper/exception/compliance_error' + +klass = ::AMA::Entity::Mapper::Type::Resolver +type_class = ::AMA::Entity::Mapper::Type::Concrete +registry_class = ::AMA::Entity::Mapper::Type::Registry +compliance_error_class = ::AMA::Entity::Mapper::Exception::ComplianceError +any_type = ::AMA::Entity::Mapper::Type::Any::INSTANCE + +describe klass do + let(:entity_class) do + Class.new do + def self.to_s + 'Entity' + end + end + end + + let(:entity) do + type_class.new(entity_class) + end + + let(:parametrized_class) do + Class.new do + def self.to_s + 'Parametrized' + end + end + end + + let(:parametrized) do + type_class.new(parametrized_class).tap do |type| + type.attribute!(:value, type.parameter!(:T)) + end + end + + let(:hash_class) do + Class.new do + def self.to_s + 'Hash (not really)' + end + end + end + + let(:hash) do + type_class.new(hash_class).tap do |type| + type.attribute!(:key, type.parameter!(:K)) + type.attribute!(:value, type.parameter!(:V)) + end + end + + let(:registry) do + registry_class.new.tap do |registry| + %i[entity parametrized hash].each do |type| + registry.register(send(type)) + end + end + end + + let(:resolver) do + klass.new(registry) + end + + describe '#resolve' do + it 'passes through simple type' do + type = resolver.resolve(entity) + expect(type).to eq(entity) + end + + it 'passes through resolved type combination' do + parameter = parametrized.parameters[:T] + definition = [parametrized, parameter => entity] + expectation = parametrized.resolve_parameter(parameter, [entity]) + type = resolver.resolve(definition) + expect(type).to eq(expectation) + end + + it 'passes through nested resolved type combination' do + parameter = parametrized.parameters[:T] + definition = [parametrized, parameter => [parametrized, parameter => entity]] + intermediate = parametrized.resolve_parameter(parameter, [entity]) + expectation = intermediate.resolve_parameter(parameter, intermediate) + type = resolver.resolve(definition) + expect(type).to eq(expectation) + end + + it 'resolves plain class' do + type = resolver.resolve(entity.type) + expect(type).to eq(entity) + end + + it 'resolves parameter' do + definition = [parametrized, T: entity] + parameter = parametrized.parameters[:T] + expectation = parametrized.resolve_parameter(parameter, [entity]) + type = resolver.resolve(definition) + expect(type).to eq(expectation) + end + + it 'resolves complex raw definition' do + definition = [hash, K: Symbol, V: [[parametrized, T: entity], entity]] + parameter = parametrized.parameters[:T] + intermediate = parametrized.resolve_parameter(parameter, [entity]) + parameter = hash.parameters[:V] + intermediate = hash.resolve_parameter(parameter, [intermediate, entity]) + parameter = hash.parameters[:K] + replacement = type_class.new(Symbol) + expectation = intermediate.resolve_parameter(parameter, replacement) + type = resolver.resolve(definition) + + expect(type).to eq(expectation) + end + + it 'raises on invalid parameters definition' do + proc = lambda do + resolver.resolve([entity, 12]) + end + + expect(&proc).to raise_error(compliance_error_class) + end + + it 'raises on invalid parameter key' do + proc = lambda do + resolver.resolve([parametrized, 12 => entity]) + end + + expect(&proc).to raise_error(compliance_error_class) + end + + it 'raises on non-existent type parameter' do + proc = lambda do + resolver.resolve([parametrized, Z: entity]) + end + + expect(&proc).to raise_error(compliance_error_class) + end + + it 'raises on foreign type parameter' do + parameter = hash.parameters[:K] + proc = lambda do + resolver.resolve([parametrized, parameter => entity]) + end + + expect(&proc).to raise_error(compliance_error_class) + end + + it 'raises on invalid type' do + proc = lambda do + resolver.resolve('cucumber') + end + + expect(&proc).to raise_error(compliance_error_class) + end + + it 'processes Any type as any other' do + resolver.resolve([parametrized, T: any_type]) + end + end +end diff --git a/test/suite/unit/mapper/type/concrete.spec.rb b/test/suite/unit/mapper/type/concrete.spec.rb index f91137a..e7fe990 100644 --- a/test/suite/unit/mapper/type/concrete.spec.rb +++ b/test/suite/unit/mapper/type/concrete.spec.rb @@ -11,17 +11,36 @@ Class.new end + let(:attribute) do + double + end + + let(:parameter) do + double + end + let(:left) do - klass.new(dummy) + klass.new(dummy).tap do |type| + type.attributes[:T] = attribute + type.parameters[:T] = parameter + end end let(:right) do klass.new(dummy).tap do |type| - type.attributes[:T] = double - type.parameters[:T] = double + type.attributes[:T] = attribute + type.parameters[:T] = parameter end end + let(:different) do + klass.new(dummy) + end + + let(:completely_different) do + klass.new(Class.new) + end + describe '#initialize' do it 'should raise error if invalid input was provided' do proc = lambda do @@ -32,13 +51,21 @@ end describe '#eql?' do - it 'should be equal to another type for same class' do + it 'should be equal to another type for same class with same attributes' do expect(left).to eq(right) end + + it 'returns false if attributes differ' do + expect(left).not_to eq(different) + end + + it 'returns false if enclosed classes differ' do + expect(different).not_to eq(completely_different) + end end describe '#hash' do - it 'should be equal among types for same class' do + it 'should be equal among types for same class with same attributes' do expect(left.hash).to eq(right.hash) end end diff --git a/test/suite/unit/mapper/type/type.spec.rb b/test/suite/unit/mapper/type/type.spec.rb index db71447..b3fc19b 100644 --- a/test/suite/unit/mapper/type/type.spec.rb +++ b/test/suite/unit/mapper/type/type.spec.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true require_relative '../../../../../lib/mapper/type' -require_relative '../../../../../lib/mapper/exception/compliance_error' require_relative '../../../../../lib/mapper/exception/mapping_error' inspected_class = ::AMA::Entity::Mapper::Type -compliance_error_class = ::AMA::Entity::Mapper::Exception::ComplianceError mapping_error_class = ::AMA::Entity::Mapper::Exception::MappingError describe inspected_class do @@ -107,21 +105,21 @@ def to_s end describe '#resolved!' do - it 'should raise compliance error if current type has any unresolved parameters' do + it 'checks attributes to determine if it is resolved' do + attribute = double(resolved!: nil) + expect(attribute).to receive(:resolved!).exactly(:once) + dummy.attributes = { id: attribute } + dummy.resolved! + end + + it 'relies on attributes only' do dummy.parameters = { value: double(owner: dummy, id: :value, resolved?: false) } proc = lambda do dummy.resolved! end - expect(&proc).to raise_error(compliance_error_class) - end - - it 'should pass call to attributes if current type has no parameters' do - attribute = double(resolved!: nil) - expect(attribute).to receive(:resolved!).exactly(:once) - dummy.attributes = { id: attribute } - dummy.resolved! + expect(&proc).not_to raise_error end end