diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8fbf7406..3f085150 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -45,7 +45,6 @@ Lint/ConstantDefinitionInBlock: Lint/EmptyBlock: Exclude: - 'spec/chewy/index/import/bulk_request_spec.rb' - - 'spec/chewy/index/witchcraft_spec.rb' - 'spec/chewy/minitest/helpers_spec.rb' - 'spec/chewy/rspec/update_index_spec.rb' - 'spec/chewy/search/scrolling_spec.rb' @@ -136,7 +135,6 @@ Naming/VariableNumber: # Offense count: 3 Style/DocumentDynamicEvalDefinition: Exclude: - - 'lib/chewy/index/witchcraft.rb' - 'lib/chewy/repository.rb' - 'lib/chewy/search/pagination/kaminari.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be00dad..2542ea0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Changes +* **Removal:** `Chewy::Index.witchcraft!` and the `Chewy::Index::Witchcraft` module have been removed. The compiled compose path (default since 8.3.0) delivers equivalent throughput without `method_source` / `parser` / `prism` / `unparser`. Remove any `witchcraft!` call from your indexes — no other changes are needed. ([@AlfonsoUceda][]) + ## 8.4.0 (2026-06-17) ### New Features diff --git a/docs/indexing.md b/docs/indexing.md index e15b06e3..68b9be3c 100644 --- a/docs/indexing.md +++ b/docs/indexing.md @@ -278,55 +278,6 @@ The compiled path also handles hash inputs, join fields, and nested object field For a small number of edge cases the compiler falls back to the legacy path automatically: `ignore_blank` fields, `geo_point` fields, indexes with a custom root value proc, and fields whose name is not a valid Ruby identifier. The fallback is transparent — the compose result is identical either way. -## Witchcraft technology (deprecated) - -> **Deprecated**. -> `witchcraft!` is retained only for compatibility with older index definitions and will be removed in a future major release. -> The compiled compose path above is the default for all indexes and delivers equivalent performance without `method_source` / `parser` / `prism` / `unparser` dependencies, with substantially lower boot-time memory (see [#644](https://github.com/toptal/chewy/issues/644)). -> Remove the `witchcraft!` call from your index — no other changes are needed. - -Witchcraft was an experimental performance optimization that used `method_source` to read each field value proc's Ruby source, parse it with `parser` or `prism`, rewrite the AST, and `Unparser`-emit a single fused lambda per index. -Concretely it compiled definitions like this: - -```ruby -index_scope Product -witchcraft! - -field :title -field :tags, value: -> { tags.map(&:name) } -field :categories do - field :name, value: -> (product, category) { category.name } - field :type, value: -> (product, category, crutch) { crutch.types[category.name] } -end -``` - -into a single lambda roughly equivalent to: - -```ruby --> (object, crutches) do - { - title: object.title, - tags: object.tags.map(&:name), - categories: object.categories.map do |object2| - { - name: object2.name, - type: crutches.types[object2.name] - } - end - } -end -``` - -It came with several limitations the compiled path does not have: - -1. Required `method_source`-parseable proc source — reflowing a value proc could break the build. -2. Did not support value procs with splat arguments. -3. Required dynamically-generated fields to use procs with explicit arguments rather than argumentless `-> { ... }`. -4. Required `method_source`, `parser` (or `prism`), and `unparser` at boot — together adding ~7 MiB of allocations and ~1 MiB of retained memory to every Rails boot, even for apps that did not call `witchcraft!`. - -Calling `witchcraft!` now prints a deprecation warning. -Removing the call switches the index to the compiled path automatically; no other code changes are needed. - ## Import context When importing, you often already have data in memory at the call site (precomputed embeddings, batched API responses, aggregations) that crutches would otherwise re-fetch from the database. @@ -355,7 +306,7 @@ field :embedding, value: ->(object, crutches, context) { Both are backward-compatible: existing 1-arg crutch blocks and 1-2 arg field procs continue to work unchanged via arity-based dispatch. -Context flows through the default compiled compose path automatically, and is also honored under the legacy `witchcraft!` path and under parallel imports (`parallel: N`). +Context flows through the default compiled compose path automatically, and is also honored under parallel imports (`parallel: N`). With `parallel:`, the same context hash is shared across all workers — precompute once on the main process, reuse in every worker. ```ruby diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 81f488a1..bd2f8c6b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -79,7 +79,6 @@ See [configuration.md](configuration.md#import-scope-clean-up-behavior) for deta Some Chewy features require additional gems that are not listed as hard dependencies: - **`parallel`** — required for `chewy:parallel:*` rake tasks. Install it with `gem 'parallel'` in your Gemfile. -- **`method_source`**, **`parser`** (or **`prism`** on Ruby 3.3+), **`unparser`** — required only for the deprecated `witchcraft!` path. Not needed for the default compiled compose path. Install them only if you still call `witchcraft!` from an index. If these gems are missing you'll get a `LoadError` when the relevant feature is used. diff --git a/gemfiles/base.gemfile b/gemfiles/base.gemfile index 02ea846b..fae7027f 100644 --- a/gemfiles/base.gemfile +++ b/gemfiles/base.gemfile @@ -1,7 +1,6 @@ gem 'database_cleaner' gem 'elasticsearch-extensions' gem 'kaminari-core', require: false -gem 'method_source' gem 'parallel', require: false gem 'rake' gem 'redis', require: false @@ -14,4 +13,3 @@ gem 'rubocop-rake' gem 'sidekiq', require: false gem 'sqlite3' gem 'timecop' -gem 'unparser', '~> 0.6.15' diff --git a/lib/chewy/index.rb b/lib/chewy/index.rb index c0da3509..52bae7ce 100644 --- a/lib/chewy/index.rb +++ b/lib/chewy/index.rb @@ -10,7 +10,6 @@ require 'chewy/index/settings' require 'chewy/index/specification' require 'chewy/index/syncer' -require 'chewy/index/witchcraft' require 'chewy/index/compiled' require 'chewy/index/wrapper' @@ -32,7 +31,6 @@ class Index include Mapping include Observe include Crutch - include Witchcraft include Compiled include Wrapper diff --git a/lib/chewy/index/compiled.rb b/lib/chewy/index/compiled.rb index 270d5fe7..909e498d 100644 --- a/lib/chewy/index/compiled.rb +++ b/lib/chewy/index/compiled.rb @@ -7,8 +7,7 @@ class Index # per-field arity into the call site and inlines hash construction, # removing the per-field iteration + dispatch overhead of the legacy # plain compose path. Proc bodies are NOT inlined — procs are called - # via `.call`. In exchange there are no parser/unparser/method_source - # deps and no Ruby/Prism version coupling. + # via `.call`. # # `fields:` selection is supported: a dedicated method is generated # and memoized per unique fields set. @@ -28,7 +27,6 @@ module ClassMethods # Returns true when this index can use the compiled path for the # given compose call. Builds the per-fields method if needed. def compiled_compose_available?(fields) - return false if witchcraft? return false unless root&.children&.present? ensure_compiled_compose_for!(fields) diff --git a/lib/chewy/index/import.rb b/lib/chewy/index/import.rb index 0432c445..b69a6751 100644 --- a/lib/chewy/index/import.rb +++ b/lib/chewy/index/import.rb @@ -117,7 +117,7 @@ def bulk(**options) payload_errors(error_items) end - # Composes a single document from the passed object. Uses either witchcraft + # Composes a single document from the passed object. Uses either the compiled # or normal composing under the hood. # # @param object [Object] a data source object @@ -140,8 +140,6 @@ def compose(object, crutches = nil, fields: [], context: {}) else compiled_compose(object, crutches, context, fields) end - elsif witchcraft? && root.children.present? - cauldron(fields: fields).brew(object, crutches, context) else root.compose(object, crutches, fields: fields, context: context) end diff --git a/lib/chewy/index/witchcraft.rb b/lib/chewy/index/witchcraft.rb deleted file mode 100644 index 9b8da649..00000000 --- a/lib/chewy/index/witchcraft.rb +++ /dev/null @@ -1,295 +0,0 @@ -module Chewy - class Index - module Witchcraft - extend ActiveSupport::Concern - - included do - class_attribute :_witchcraft, instance_reader: false, instance_writer: false - end - - def self.load_dependencies! - return if @dependencies_loaded - - require 'method_source' - begin - require 'prism' - rescue LoadError - require 'parser/current' - end - require 'unparser' - @dependencies_loaded = true - rescue LoadError - nil - end - - module ClassMethods - def witchcraft! - warn( - '[DEPRECATION] Chewy::Index.witchcraft! is deprecated and will be removed in a future release. ' \ - 'The compiled compose path is now the default and delivers equivalent performance without ' \ - "method_source/parser/unparser dependencies. Remove the `witchcraft!` call from #{name || self}.", - uplevel: 1 - ) - Witchcraft.load_dependencies! - 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) - if RUBY_VERSION >= '3.3' - messages << "Prism gem is required for the Witchcraft, please add `gem 'prism'` to your Gemfile" unless '::Prism'.safe_constantize - else - messages << "Parser gem is required for the Witchcraft, please add `gem 'parser'` to your Gemfile" unless '::Parser'.safe_constantize - end - 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] restricts the fields for composition - def initialize(index, fields: []) - @index = index - @locals = [] - @fields = fields - end - - def brew(object, crutches = nil, context = {}) - alicorn.call(locals, object, crutches, context).as_json - end - - private - - def alicorn - @alicorn ||= singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - -> (locals, object0, crutches, context) 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) # rubocop:disable Metrics/AbcSize - lambdas = extract_lambdas(ast_from_proc(proc)) - - 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] - source = replace_lvar(source, proc_params[nesting + 2], :context) if proc_params[nesting + 2] - - 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 ast_from_proc(proc) - if defined?(Prism) - Prism::Translation::ParserCurrent.parse(proc.source) - else - Parser::CurrentRuby.parse(proc.source) - end - end - - def extract_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| extract_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 diff --git a/spec/chewy/index/compiled_spec.rb b/spec/chewy/index/compiled_spec.rb index e1aacef9..ae708538 100644 --- a/spec/chewy/index/compiled_spec.rb +++ b/spec/chewy/index/compiled_spec.rb @@ -175,21 +175,6 @@ def self.mapping(&block) expect(index.compose(obj)).to eq('first-name' => 'x') end end - - context 'witchcraft fallback' do - mapping do - field :name - end - - it 'uses witchcraft path when witchcraft! is set (with deprecation)' do - # silence the deprecation noise - original_stderr = $stderr - $stderr = StringIO.new - index.witchcraft! - $stderr = original_stderr - expect(index.compiled_compose_available?([])).to eq(false) - end - end end describe 'dispatch in Import.compose' do diff --git a/spec/chewy/index/import/bulk_builder_spec.rb b/spec/chewy/index/import/bulk_builder_spec.rb index 41645c88..de19b5b4 100644 --- a/spec/chewy/index/import/bulk_builder_spec.rb +++ b/spec/chewy/index/import/bulk_builder_spec.rb @@ -169,15 +169,6 @@ def derived {index: {_id: 42, data: {'name' => 'Name42'}}} ]) end - - context 'witchcraft' do - before { CitiesIndex.witchcraft! } - specify do - expect(subject.bulk_body).to eq([ - {index: {_id: 42, data: {'name' => 'Name42'}}} - ]) - end - end end context 'context' do @@ -240,28 +231,6 @@ def derived end end end - - context 'witchcraft' do - before { CitiesIndex.witchcraft! } - - context 'without context' do - specify do - expect(subject.bulk_body).to eq([ - {index: {_id: 42, data: {'name' => 'Name42'}}} - ]) - end - end - - context 'with context' do - subject { described_class.new(index, to_index: to_index, delete: delete, fields: fields, context: {names: {42 => 'ContextName42'}}) } - - specify do - expect(subject.bulk_body).to eq([ - {index: {_id: 42, data: {'name' => 'ContextName42'}}} - ]) - end - end - end end context 'empty ids' do diff --git a/spec/chewy/index/import_spec.rb b/spec/chewy/index/import_spec.rb index 7d286f68..0fcea2fa 100644 --- a/spec/chewy/index/import_spec.rb +++ b/spec/chewy/index/import_spec.rb @@ -619,20 +619,6 @@ def imported_comments .to eq('name' => 'Name42') end - context 'witchcraft' do - before { CitiesIndex.witchcraft! } - - specify do - expect(CitiesIndex.compose(double(name: 'Name', rating: 42))) - .to eq('name' => 'Name42', 'rating' => 42) - end - - specify do - expect(CitiesIndex.compose(double(name: 'Name', rating: 42), fields: %i[name])) - .to eq('name' => 'Name42') - end - end - context 'custom crutches' do let(:crutches) { double(names: {'Name' => 'Name43'}) } diff --git a/spec/chewy/index/witchcraft_spec.rb b/spec/chewy/index/witchcraft_spec.rb deleted file mode 100644 index d25f4ce7..00000000 --- a/spec/chewy/index/witchcraft_spec.rb +++ /dev/null @@ -1,258 +0,0 @@ -require 'spec_helper' - -describe Chewy::Index::Witchcraft do - def self.mapping(&block) - before do - stub_index(:products) do - witchcraft! - - instance_exec(&block) - end - end - end - - describe '#cauldron' do - let(:index) { ProductsIndex } - let(:object) {} - - context 'empty mapping' do - mapping {} - specify { expect(index.cauldron.brew(object)).to eq({}) } - end - - context do - mapping do - field :name - field :age - field :tags - end - let(:attributes) { {name: 'Name', age: 13, tags: %w[Ruby RoR]} } - - context do - let(:object) { double(attributes) } - specify { expect(index.cauldron.brew(object)).to eq(attributes.as_json) } - end - - context do - let(:object) { attributes } - specify { expect(index.cauldron.brew(object)).to eq(attributes.as_json) } - end - - context do - let(:object) { double(attributes) } - specify do - expect(index.cauldron(fields: %i[name tags]).brew(object)) - .to eq('name' => 'Name', 'tags' => %w[Ruby RoR]) - end - end - - context do - let(:object) { attributes } - specify do - expect(index.cauldron(fields: %i[name tags]).brew(object)) - .to eq('name' => 'Name', 'tags' => %w[Ruby RoR]) - end - end - end - - context 'simple lambdas' do - mapping do - field :name - field :age, value: lambda { |obj| - obj.age if obj - } - field :tags, value: -> { tags.map(&:to_sym) } - end - let(:attributes) { {name: 'Name', age: 13, tags: %w[Ruby RoR]} } - - context do - let(:object) { double(attributes) } - specify { expect(index.cauldron.brew(object)).to eq(attributes.merge(tags: %i[Ruby RoR]).as_json) } - end - end - - context 'crutches' do - mapping do - field :name, value: ->(_o, c) { c.names[0] } - end - let(:attributes) { {name: 'Name'} } - - context do - let(:object) { double(attributes) } - let(:crutches) { double(names: ['Other']) } - specify { expect(index.cauldron.brew(object, crutches)).to eq({name: 'Other'}.as_json) } - end - end - - context 'context' do - mapping do - field :name, value: ->(_o, _c, ctx) { ctx[:name] } - end - - context do - let(:object) { double(name: 'Name') } - let(:crutches) { double } - let(:context) { {name: 'FromContext'} } - specify { expect(index.cauldron.brew(object, crutches, context)).to eq({name: 'FromContext'}.as_json) } - end - end - - context 'nesting' do - context do - mapping do - field :queries do - field :title - field :body, value: -> { "This #{self[:body]}" } - end - end - - let(:object) do - double(queries: [ - {title: 'Title1', body: 'Body1'}, - {title: 'Title2', body: 'Body2'} - ]) - end - specify do - expect(index.cauldron.brew(object)).to eq({queries: [ - {title: 'Title1', body: 'This Body1'}, - {title: 'Title2', body: 'This Body2'} - ]}.as_json) - end - end - - context do - mapping do - field :queries do - field :title - field :body, value: -> { "This #{body}" } - end - end - - let(:object) do - double(queries: [ - double(title: 'Title1', body: 'Body1'), - double(title: 'Title2', body: 'Body2') - ]) - end - specify do - expect(index.cauldron.brew(object)).to eq({queries: [ - {title: 'Title1', body: 'This Body1'}, - {title: 'Title2', body: 'This Body2'} - ]}.as_json) - end - end - - context do - mapping do - field :queries, value: -> { queries } do - field :title - field :body, value: -> { "This #{body}" } - end - end - - let(:object) do - double(queries: [ - double(title: 'Title1', body: 'Body1'), - double(title: 'Title2', body: 'Body2') - ]) - end - specify do - expect(index.cauldron.brew(object)).to eq({queries: [ - {title: 'Title1', body: 'This Body1'}, - {title: 'Title2', body: 'This Body2'} - ]}.as_json) - end - end - - context do - mapping do - field :queries do - field :fields, value: ->(_o, q) { q.fields } do - field :first - field :second, value: lambda { |_o, q, f, c| - q.value + (f.respond_to?(:second) ? f.second : c.second) - } - end - end - end - - let(:object) do - double(queries: [ - double(value: 'Value1', fields: [double(first: 'First1', second: 'Second1'), {first: 'First2'}]), - double(value: 'Value2', fields: double(first: 'First3', second: 'Second2', third: 'Third')) - ]) - end - specify do - expect(index.cauldron.brew(object, double(second: 'Crutch'))).to eq({queries: [ - {fields: [ - {first: 'First1', second: 'Value1Second1'}, - {first: 'First2', second: 'Value1Crutch'} - ]}, - {fields: {first: 'First3', second: 'Value2Second2'}} - ]}.as_json) - end - end - - context do - mapping do - field :not_present do - field :nothing - end - end - - let(:object) do - double(not_present: nil) - end - specify do - expect(index.cauldron.brew(object)).to eq({not_present: nil}.as_json) - end - end - end - - context 'dynamic fields' do - mapping do - [[:name], [:age]].each do |param| - field param.first, value: ->(o) { o.send(param[0]) if param[0] } - end - end - let(:attributes) { {name: 'Name', age: 13} } - - context do - let(:object) { double(attributes) } - specify { expect(index.cauldron.brew(object)).to eq(attributes.as_json) } - end - end - - context 'dynamic fields' do - mapping do - def self.custom_value(field) - ->(o) { o.send(field) } - end - - field :name, value: custom_value(:name) - end - let(:attributes) { {name: 'Name'} } - - context do - let(:object) { double(attributes) } - specify { expect(index.cauldron.brew(object)).to eq(attributes.as_json) } - end - end - - context 'symbol value' do - mapping do - field :name, value: :last_name - end - - context do - let(:object) { double(last_name: 'Name') } - specify { expect(index.cauldron.brew(object)).to eq({name: 'Name'}.as_json) } - end - - context do - let(:object) { {'last_name' => 'Name'} } - specify { expect(index.cauldron.brew(object)).to eq({name: 'Name'}.as_json) } - end - end - end -end