class Chewy::Index::Witchcraft::Cauldron

Attributes

locals[R]

Public Class Methods

new(index, fields: []) click to toggle source

@param index [Chewy::Index] index for composition @param fields [Array<Symbol>] restricts the fields for composition

# File lib/chewy/index/witchcraft.rb, line 48
def initialize(index, fields: [])
  @index = index
  @locals = []
  @fields = fields
end

Public Instance Methods

brew(object, crutches = nil) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 54
def brew(object, crutches = nil)
  alicorn.call(locals, object, crutches).as_json
end

Private Instance Methods

alicorn() click to toggle source
# File lib/chewy/index/witchcraft.rb, line 60
        def alicorn
          @alicorn ||= singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
            -> (locals, object0, crutches) do
              #{composed_values(@index.root, 0)}
            end
          RUBY
        end
binding_variable_list(node) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 257
def binding_variable_list(node)
  return unless node.is_a?(Parser::AST::Node)

  if node.type == :send && node.children[0].nil?
    node.children[1]
  else
    node.children.map { |child| binding_variable_list(child) }.flatten.compact.uniq
  end
end
composed_value(field, fetcher, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 77
        def composed_value(field, fetcher, nesting)
          nesting = nesting.next
          if field.children.present? && !field.multi_field?
            <<-RUBY
              (result#{nesting} = #{fetcher}
              if result#{nesting}.nil?
                nil
              elsif result#{nesting}.respond_to?(:to_ary)
                result#{nesting}.map do |object#{nesting}|
                  #{composed_values(field, nesting)}
                end
              else
                object#{nesting} = result#{nesting}
                #{composed_values(field, nesting)}
              end)
            RUBY
          else
            fetcher
          end
        end
composed_values(field, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 68
        def composed_values(field, nesting)
          source = <<-RUBY
            non_proc_values#{nesting} = #{non_proc_values(field, nesting)}
            proc_values#{nesting} = #{proc_values(field, nesting)}
            non_proc_values#{nesting}.merge!(proc_values#{nesting})
          RUBY
          source.gsub("\n,", ',')
        end
exctract_lambdas(node) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 195
def exctract_lambdas(node)
  return unless node.is_a?(Parser::AST::Node)

  if node.type == :block && node.children[0].type == :send && node.children[0].to_a == [nil, :lambda]
    [node.children[2]]
  else
    node.children.map { |child| exctract_lambdas(child) }.flatten.compact
  end
end
non_proc_fields_for(parent, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 142
def non_proc_fields_for(parent, nesting)
  return [] unless parent

  fields = (parent.children || []).reject { |field| field.value.is_a?(Proc) }

  if nesting.zero? && @fields.present?
    fields.select { |f| @fields.include?(f.name) }
  else
    fields
  end
end
non_proc_values(field, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 98
        def non_proc_values(field, nesting)
          non_proc_fields = non_proc_fields_for(field, nesting)
          object = "object#{nesting}"

          if non_proc_fields.present?
            <<-RUBY
              (if #{object}.is_a?(Hash)
                {
                  #{non_proc_fields.map do |f|
                    key_name = f.value.is_a?(Symbol) || f.value.is_a?(String) ? f.value : f.name
                    fetcher = "#{object}.has_key?(:#{key_name}) ? #{object}[:#{key_name}] : #{object}['#{key_name}']"
                    "'#{f.name}'.freeze => #{composed_value(f, fetcher, nesting)}"
                  end.join(', ')}
                }
              else
                {
                  #{non_proc_fields.map do |f|
                    method_name = f.value.is_a?(Symbol) || f.value.is_a?(String) ? f.value : f.name
                    "'#{f.name}'.freeze => #{composed_value(f, "#{object}.#{method_name}", nesting)}"
                  end.join(', ')}
                }
              end)
            RUBY
          else
            '{}'
          end
        end
proc_fields_for(parent, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 154
def proc_fields_for(parent, nesting)
  return [] unless parent

  fields = (parent.children || []).select { |field| field.value.is_a?(Proc) }

  if nesting.zero? && @fields.present?
    fields.select { |f| @fields.include?(f.name) }
  else
    fields
  end
end
proc_values(field, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 126
        def proc_values(field, nesting)
          proc_fields = proc_fields_for(field, nesting)

          if proc_fields.present?
            <<-RUBY
              {
                #{proc_fields.map do |f|
                  "'#{f.name}'.freeze => (#{composed_value(f, source_for(f.value, nesting), nesting)})"
                end.join(', ')}
              }
            RUBY
          else
            '{}'
          end
        end
replace_local(node, variable, local_index) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 241
def replace_local(node, variable, local_index)
  if node.is_a?(Parser::AST::Node)
    if node.type == :send && node.children.to_a == [nil, variable]
      node.updated(nil, [
        Parser::AST::Node.new(:lvar, [:locals]),
        :[],
        Parser::AST::Node.new(:int, [local_index])
      ])
    else
      node.updated(nil, node.children.map { |child| replace_local(child, variable, local_index) })
    end
  else
    node
  end
end
replace_lvar(node, old_variable, new_variable) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 205
def replace_lvar(node, old_variable, new_variable)
  if node.is_a?(Parser::AST::Node)
    if node.type == :lvar && node.children.to_a == [old_variable]
      node.updated(nil, [new_variable])
    else
      node.updated(nil, node.children.map { |child| replace_lvar(child, old_variable, new_variable) })
    end
  else
    node
  end
end
replace_self(node, variable) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 229
def replace_self(node, variable)
  if node.is_a?(Parser::AST::Node)
    if node.type == :self
      Parser::AST::Node.new(:lvar, [variable])
    else
      node.updated(nil, node.children.map { |child| replace_self(child, variable) })
    end
  else
    node
  end
end
replace_send(node, variable) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 217
def replace_send(node, variable)
  if node.is_a?(Parser::AST::Node)
    if node.type == :send && node.children[0].nil?
      node.updated(nil, [Parser::AST::Node.new(:lvar, [variable]), *node.children[1..]])
    else
      node.updated(nil, node.children.map { |child| replace_send(child, variable) })
    end
  else
    node
  end
end
source_for(proc, nesting) click to toggle source
# File lib/chewy/index/witchcraft.rb, line 166
def source_for(proc, nesting)
  ast = Parser::CurrentRuby.parse(proc.source)
  lambdas = exctract_lambdas(ast)

  raise "No lambdas found, try to reformat your code:\n`#{proc.source}`" unless lambdas

  source = lambdas.first
  proc_params = proc.parameters.map(&:second)

  if proc.arity.zero?
    source = replace_self(source, :"object#{nesting}")
    source = replace_send(source, :"object#{nesting}")
  elsif proc.arity.negative?
    raise "Splat arguments are unsupported by witchcraft:\n`#{proc.source}"
  else
    (nesting + 1).times do |n|
      source = replace_lvar(source, proc_params[n], :"object#{n}") if proc_params[n]
    end
    source = replace_lvar(source, proc_params[nesting + 1], :crutches) if proc_params[nesting + 1]

    binding_variable_list(source).each do |variable|
      locals.push(proc.binding.eval(variable.to_s))
      source = replace_local(source, variable, locals.size - 1)
    end
  end

  Unparser.unparse(source)
end