# An indexing step definition, including it's source location # for logging # # This one represents an "each_record" step, a subclass below # for "to_field" # # source_location is just a string with filename and line number for # showing to devs in debugging. class Traject::Indexer class EachRecordStep attr_accessor :source_location, :block attr_reader :lambda EMPTY_ACCUMULATOR = [].freeze def initialize(lambda, block, source_location) self.lambda = lambda self.block = block self.source_location = source_location self.validate! end def to_field_step? false end # Set the arity of the lambda expression just once, when we define it def lambda=(lam) @lambda_arity = 0 # assume return unless lam @lambda = lam if @lambda.is_a?(Proc) @lambda_arity = @lambda.arity else raise NamingError.new("argument to each_record must be a block/lambda, not a #{lam.class} #{self.inspect}") end end # raises if bad data def validate! unless self.lambda or self.block raise ArgumentError.new("Missing Argument: each_record must take a block/lambda as an argument (#{self.inspect})") end [self.lambda, self.block].each do |proc| # allow negative arity, meaning variable/optional, trust em on that. # but for positive arrity, we need 1 or 2 args if proc unless proc.is_a?(Proc) raise NamingError.new("argument to each_record must be a block/lambda, not a #{proc.class} #{self.inspect}") end if (proc.arity == 0 || proc.arity > 2) raise ArityError.new("block/proc given to each_record needs 1 or 2 arguments: #{self.inspect}") end end end end # For each_record, always return an empty array as the # accumulator, since it doesn't have those kinds of side effects def execute(context) sr = context.source_record if @lambda if @lambda_arity == 1 @lambda.call(sr) else @lambda.call(sr, context) end end if @block @block.call(sr, context) end return EMPTY_ACCUMULATOR # empty -- no accumulator for each_record end # Over-ride inspect for outputting error messages etc. def inspect "(each_record at #{source_location})" end end # An indexing step definition for a "to_field" step to specific # field. class ToFieldStep attr_accessor :field_name, :block, :source_location attr_reader :lambda def initialize(fieldname, lambda, block, source_location) self.field_name = fieldname.freeze self.lambda = lambda self.block = block self.source_location = source_location validate! end def to_field_step? true end def lambda=(lam) @lambda = lam @lambda_arity = @lambda ? @lambda.arity : 0 end def validate! if self.field_name.nil? || !self.field_name.is_a?(String) || self.field_name.empty? raise NamingError.new("to_field requires the field name (as a string) as the first argument at #{self.source_location})") end [self.lambda, self.block].each do |proc| # allow negative arity, meaning variable/optional, trust em on that. # but for positive arrity, we need 2 or 3 args if proc && (proc.arity == 0 || proc.arity == 1 || proc.arity > 3) raise ArityError.new("error parsing field '#{self.field_name}': block/proc given to to_field needs 2 or 3 (or variable) arguments: #{proc} (#{self.inspect})") end end end # Override inspect for developer debug messages def inspect "(to_field #{self.field_name} at #{self.source_location})" end def execute(context) accumulator = [] sr = context.source_record if @lambda if @lambda_arity == 2 @lambda.call(sr, accumulator) else @lambda.call(sr, accumulator, context) end end if @block @block.call(sr, accumulator, context) end return accumulator end end # A class representing a block of logic called after # processing, registered with #after_processing class AfterProcessingStep attr_accessor :lambda, :block, :source_location def initialize(lambda, block, source_location) self.lambda = lambda self.block = block self.source_location = source_location end def to_field_step? false end # after_processing steps get no args yielded to # their blocks, they just are what they are. def execute @block.call if @block @lambda.call if @lambda end def inspect "(after_processing at #{self.source_location}" end end end