require 'transproc/all' require 'rom/processor' module ROM class Processor # Data mapping transformer builder using Transproc # # This builds a transproc function that is used to map a whole relation # # @see https://github.com/solnic/transproc too # # @private class Transproc < Processor include ::Transproc::Composer module Functions extend ::Transproc::Registry import ::Transproc::Coercions import ::Transproc::ArrayTransformations import ::Transproc::HashTransformations import ::Transproc::ClassTransformations import ::Transproc::ProcTransformations def self.identity(tuple) tuple end def self.filter_empty(arr) arr.reject { |row| row.values.all?(&:nil?) } end def self.inject_union_value(tuple, name, keys, coercer) values = tuple.values_at(*keys) result = coercer.call(*values) tuple.merge(name => result) end end # @return [Mapper] mapper that this processor belongs to # # @api private attr_reader :mapper # @return [Header] header from a mapper # # @api private attr_reader :header # @return [Class] model class from a mapper # # @api private attr_reader :model # @return [Hash] header's attribute mapping # # @api private attr_reader :mapping # @return [Proc] row-processing proc # # @api private attr_reader :row_proc # Build a transproc function from the header # # @param [ROM::Header] header # # @return [Transproc::Function] # # @api private def self.build(mapper, header) new(mapper, header).to_transproc end # @api private def initialize(mapper, header) @mapper = mapper @header = header @model = header.model @mapping = header.mapping initialize_row_proc end # Coerce mapper header to a transproc data mapping function # # @return [Transproc::Function] # # @api private def to_transproc compose(t(:identity)) do |ops| combined = header.combined ops << t(:combine, combined.map(&method(:combined_args))) if combined.any? ops << header.preprocessed.map { |attr| visit(attr, true) } ops << t(:map_array, row_proc) if row_proc ops << header.postprocessed.map { |attr| visit(attr, true) } end end private # Visit an attribute from the header # # This forwards to a specialized visitor based on the attribute type # # @param [Header::Attribute] attribute # @param [Array] args Allows to send `preprocess: true` # # @api private def visit(attribute, *args) type = attribute.class.name.split('::').last.downcase send("visit_#{type}", attribute, *args) end # Visit plain attribute # # It will call block transformation if it's used # # If it's a typed attribute a coercion transformation is added # # @param [Header::Attribute] attribute # # @api private def visit_attribute(attribute) coercer = attribute.meta[:coercer] if attribute.union? compose do |ops| ops << t(:inject_union_value, attribute.name, attribute.key, coercer) ops << t(:reject_keys, attribute.key) end elsif coercer t(:map_value, attribute.name, t(:bind, mapper, coercer)) elsif attribute.typed? t(:map_value, attribute.name, t(:"to_#{attribute.type}")) end end # Visit hash attribute # # @param [Header::Attribute::Hash] attribute # # @api private def visit_hash(attribute) with_row_proc(attribute) do |row_proc| t(:map_value, attribute.name, row_proc) end end # Visit combined attribute # # @api private def visit_combined(attribute) op = with_row_proc(attribute) do |row_proc| array_proc = if attribute.type == :hash t(:map_array, row_proc) >> -> arr { arr.first } else t(:map_array, row_proc) end t(:map_value, attribute.name, array_proc) end if op op elsif attribute.type == :hash t(:map_value, attribute.name, -> arr { arr.first }) end end # Visit array attribute # # @param [Header::Attribute::Array] attribute # # @api private def visit_array(attribute) with_row_proc(attribute) do |row_proc| t(:map_value, attribute.name, t(:map_array, row_proc)) end end # Visit wrapped hash attribute # # :nest transformation is added to handle wrapping # # @param [Header::Attribute::Wrap] attribute # # @api private def visit_wrap(attribute) name = attribute.name keys = attribute.tuple_keys compose do |ops| ops << t(:nest, name, keys) ops << visit_hash(attribute) end end # Visit unwrap attribute # # :unwrap transformation is added to handle unwrapping # # @param [Header::Attributes::Unwrap] # # @api private def visit_unwrap(attribute) name = attribute.name keys = attribute.pop_keys compose do |ops| ops << visit_hash(attribute) ops << t(:unwrap, name, keys) end end # Visit group hash attribute # # :group transformation is added to handle grouping during preprocessing. # Otherwise we simply use array visitor for the attribute. # # @param [Header::Attribute::Group] attribute # @param [Boolean] preprocess true if we are building a relation preprocessing # function that is applied to the whole relation # # @api private def visit_group(attribute, preprocess = false) if preprocess name = attribute.name header = attribute.header keys = attribute.tuple_keys others = header.preprocessed compose do |ops| ops << t(:group, name, keys) ops << t(:map_array, t(:map_value, name, t(:filter_empty))) ops << others.map { |attr| t(:map_array, t(:map_value, name, visit(attr, true))) } end else visit_array(attribute) end end # Visit ungroup attribute # # :ungroup transforation is added to handle ungrouping during preprocessing. # Otherwise we simply use array visitor for the attribute. # # @param [Header::Attribute::Ungroup] attribute # @param [Boolean] preprocess true if we are building a relation preprocessing # function that is applied to the whole relation # # @api private def visit_ungroup(attribute, preprocess = false) if preprocess name = attribute.name header = attribute.header keys = attribute.pop_keys others = header.postprocessed compose do |ops| ops << others.map { |attr| t(:map_array, t(:map_value, name, visit(attr, true))) } ops << t(:ungroup, name, keys) end else visit_array(attribute) end end # Visit fold hash attribute # # :fold transformation is added to handle folding during preprocessing. # # @param [Header::Attribute::Fold] attribute # @param [Boolean] preprocess true if we are building a relation preprocessing # function that is applied to the whole relation # # @api private def visit_fold(attribute, preprocess = false) if preprocess name = attribute.name keys = attribute.tuple_keys compose do |ops| ops << t(:group, name, keys) ops << t(:map_array, t(:map_value, name, t(:filter_empty))) ops << t(:map_array, t(:fold, name, keys.first)) end end end # Visit unfold hash attribute # # :unfold transformation is added to handle unfolding during preprocessing. # # @param [Header::Attribute::Unfold] attribute # @param [Boolean] preprocess true if we are building a relation preprocessing # function that is applied to the whole relation # # @api private def visit_unfold(attribute, preprocess = false) if preprocess name = attribute.name header = attribute.header keys = attribute.pop_keys key = keys.first others = header.postprocessed compose do |ops| ops << others.map { |attr| t(:map_array, t(:map_value, name, visit(attr, true))) } ops << t(:map_array, t(:map_value, name, t(:insert_key, key))) ops << t(:map_array, t(:reject_keys, [key] - [name])) ops << t(:ungroup, name, [key]) end end end # Visit excluded attribute # # @param [Header::Attribute::Exclude] attribute # # @api private def visit_exclude(attribute) t(:reject_keys, [attribute.name]) end # @api private def combined_args(attribute) other = attribute.header.combined if other.any? children = other.map(&method(:combined_args)) [attribute.name, attribute.meta[:keys], children] else [attribute.name, attribute.meta[:keys]] end end # Build row_proc # # This transproc function is applied to each row in a dataset # # @api private def initialize_row_proc @row_proc = compose { |ops| alias_handler = header.copy_keys ? :copy_keys : :rename_keys process_header_keys(ops) ops << t(alias_handler, mapping) if header.aliased? ops << header.map { |attr| visit(attr) } ops << t(:constructor_inject, model) if model } end # Process row_proc header keys # # @api private def process_header_keys(ops) if header.reject_keys all_keys = header.tuple_keys + header.non_primitives.map(&:key) ops << t(:accept_keys, all_keys) end ops end # Yield row proc for a given attribute if any # # @param [Header::Attribute] attribute # # @api private def with_row_proc(attribute) row_proc = row_proc_from(attribute) yield(row_proc) if row_proc end # Build a row_proc from a given attribute # # This is used by embedded attribute visitors # # @api private def row_proc_from(attribute) new(mapper, attribute.header).row_proc end # Return a new instance of the processor # # @api private def new(*args) self.class.new(*args) end # @api private def t(*args) Functions[*args] end end end end