lib/chewy/index/witchcraft.rb
begin
require 'method_source'
require 'parser/current'
require 'unparser'
rescue LoadError
nil
end
module Chewy
class Index
module Witchcraft
extend ActiveSupport::Concern
included do
class_attribute :_witchcraft, instance_reader: false, instance_writer: false
end
module ClassMethods
def witchcraft!
self._witchcraft = true
check_requirements!
end
def check_requirements!
messages = []
messages << "MethodSource gem is required for the Witchcraft, please add `gem 'method_source'` to your Gemfile" unless Proc.method_defined?(:source)
messages << "Parser gem is required for the Witchcraft, please add `gem 'parser'` to your Gemfile" unless '::Parser'.safe_constantize
messages << "Unparser gem is required for the Witchcraft, please add `gem 'unparser'` to your Gemfile" unless '::Unparser'.safe_constantize
messages = messages.join("\n")
raise messages if messages.present?
end
def witchcraft?
!!_witchcraft
end
def cauldron(**options)
(@cauldron ||= {})[options] ||= Cauldron.new(self, **options)
end
end
class Cauldron
attr_reader :locals
# @param index [Chewy::Index] index for composition
# @param fields [Array<Symbol>] restricts the fields for composition
def initialize(index, fields: [])
@index = index
@locals = []
@fields = fields
end
def brew(object, crutches = nil)
alicorn.call(locals, object, crutches).as_json
end
private
def alicorn
@alicorn ||= singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-> (locals, object0, crutches) do
#{composed_values(@index.root, 0)}
end
RUBY
end
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
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
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
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
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
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
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
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
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
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
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
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
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
end
end
end
end