Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor lib/meilisearch.rb #379

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 39 additions & 335 deletions lib/meilisearch-rails.rb

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions lib/meilisearch/rails/index_settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
module MeiliSearch
module Rails
class IndexSettings
DEFAULT_BATCH_SIZE = 1000

DEFAULT_PRIMARY_KEY = 'id'.freeze

# Meilisearch settings
OPTIONS = %i[
searchable_attributes
filterable_attributes
sortable_attributes
displayed_attributes
distinct_attribute
synonyms
stop_words
ranking_rules
attributes_to_highlight
attributes_to_crop
crop_length
pagination
faceting
typo_tolerance
proximity_precision
].freeze

CAMELIZE_OPTIONS = %i[pagination faceting typo_tolerance].freeze

OPTIONS.each do |option|
define_method option do |value|
instance_variable_set("@#{option}", value)
end

underscored_name = option.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
alias_method underscored_name, option if underscored_name != option
end

def initialize(options, &block)
@options = options
instance_exec(&block) if block_given?
warn_searchable_missing_attributes
end

def warn_searchable_missing_attributes
searchables = get_setting(:searchable_attributes)&.map { |searchable| searchable.to_s.split('.').first }
attrs = get_setting(:attributes)&.map { |k, _| k.to_s }

if searchables.present? && attrs.present?
(searchables - attrs).each do |missing_searchable|
warning = <<~WARNING
[meilisearch-rails] #{missing_searchable} declared in searchable_attributes but not in attributes. \
Please add it to attributes if it should be searchable.
WARNING
MeiliSearch::Rails.logger.warn(warning)
end
end
end

def use_serializer(serializer)
@serializer = serializer
# instance_variable_set("@serializer", serializer)
end

def attribute(*names, &block)
raise ArgumentError, 'Cannot pass multiple attribute names if block given' if block_given? && (names.length > 1)

@attributes ||= {}
names.flatten.each do |name|
@attributes[name.to_s] = block_given? ? proc { |d| d.instance_eval(&block) } : proc { |d| d.send(name) }
end
end
alias attributes attribute

def add_attribute(*names, &block)
raise ArgumentError, 'Cannot pass multiple attribute names if block given' if block_given? && (names.length > 1)

@additional_attributes ||= {}
names.each do |name|
@additional_attributes[name.to_s] = block_given? ? proc { |d| d.instance_eval(&block) } : proc { |d| d.send(name) }
end
end
alias add_attributes add_attribute

def mongoid?(document)
defined?(::Mongoid::Document) && document.class.include?(::Mongoid::Document)
end

def sequel?(document)
defined?(::Sequel::Model) && document.class < ::Sequel::Model
end

def active_record?(document)
!mongoid?(document) && !sequel?(document)
end

def get_default_attributes(document)
if mongoid?(document)
# work-around mongoid 2.4's unscoped method, not accepting a block
document.attributes

Check warning on line 99 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L99

Added line #L99 was not covered by tests
elsif sequel?(document)
document.to_hash
else
document.class.unscoped do
document.attributes
end
end
end

def get_attribute_names(document)
get_attributes(document).keys
end

def attributes_to_hash(attributes, document)
if attributes
attributes.to_h { |name, value| [name.to_s, value.call(document)] }
else
{}

Check warning on line 117 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L117

Added line #L117 was not covered by tests
end
end

def get_attributes(document)
# If a serializer is set, we ignore attributes
# everything should be done via the serializer
if [email protected]?
attributes = @serializer.new(document).attributes
elsif @attributes.blank?
attributes = get_default_attributes(document)
# no `attribute ...` have been configured, use the default attributes of the model
elsif active_record?(document)
# at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
document.class.unscoped do
attributes = attributes_to_hash(@attributes, document)
end
else
attributes = attributes_to_hash(@attributes, document)

Check warning on line 135 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L135

Added line #L135 was not covered by tests
end

attributes.merge!(attributes_to_hash(@additional_attributes, document)) if @additional_attributes

if @options[:sanitize]
attributes = sanitize_attributes(attributes)
end

attributes = encode_attributes(attributes) if @options[:force_utf8_encoding]

attributes
end

def sanitize_attributes(value)
case value
when String
ActionView::Base.full_sanitizer.sanitize(value)
when Hash
value.each { |key, val| value[key] = sanitize_attributes(val) }
when Array
value.map { |item| sanitize_attributes(item) }

Check warning on line 156 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L156

Added line #L156 was not covered by tests
else
value
end
end

def encode_attributes(value)
case value
when String
value.force_encoding('utf-8')
when Hash
value.each { |key, val| value[key] = encode_attributes(val) }
when Array
value.map { |x| encode_attributes(x) }

Check warning on line 169 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L169

Added line #L169 was not covered by tests
else
value

Check warning on line 171 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L171

Added line #L171 was not covered by tests
end
end

def get_setting(name)
instance_variable_get("@#{name}")
end

def camelize_keys(hash)
hash.transform_keys { |key| key.to_s.camelize(:lower) }
end

def to_settings
settings = {}
OPTIONS.each do |k|
v = get_setting(k)
next if v.nil?

settings[k] = if CAMELIZE_OPTIONS.include?(k) && v.is_a?(Hash)
v = camelize_keys(v)

# camelize keys of nested hashes
v.each do |key, value|
v[key] = camelize_keys(value) if value.is_a?(Hash)
end
else
v
end
end
settings
end

def add_index(index_uid, options = {}, &block)
raise ArgumentError, 'No block given' unless block_given?
if options[:auto_index] || options[:auto_remove]
raise ArgumentError, 'Options auto_index and auto_remove cannot be set on nested indexes'

Check warning on line 206 in lib/meilisearch/rails/index_settings.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/index_settings.rb#L206

Added line #L206 was not covered by tests
end

@additional_indexes ||= {}
options[:index_uid] = index_uid
@additional_indexes[options] = IndexSettings.new(options, &block)
end

def additional_indexes
@additional_indexes || {}
end
end
end
end
41 changes: 41 additions & 0 deletions lib/meilisearch/rails/model_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module MeiliSearch
module Rails
class ModelConfiguration
attr_reader :model

def initialize(model, options = {})
@model = model
parse_options(options)
end

def sequel_model?
defined?(::Sequel::Model) && model < Sequel::Model
end

def active_record_model?
defined?(::ActiveRecord) && model.ancestors.include?(::ActiveRecord::Base)
end

private

def parse_options(options)
refute_global_options(options, [:per_environment])
mutually_exclusive_options(options, [:enqueue, :synchronous])
end

def refute_global_options(options, misapplied_global_opts)
misapplied_global_opts.each do |opt|
if options[opt]
raise BadConfiguration, ":#{opt} option should be defined globally on MeiliSearch::Rails.configuration block."
end
end
end

def mutually_exclusive_options(options, exclusives)
first, second = exclusives.select { |opt| options[opt] }

raise ArgumentError, "Cannot use :#{first} if the :#{second} option is set" if second
end
end
end
end
82 changes: 82 additions & 0 deletions lib/meilisearch/rails/safe_index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module MeiliSearch
module Rails
# this class wraps an MeiliSearch::Index document ensuring all raised exceptions
# are correctly logged or thrown depending on the `raise_on_failure` option
class SafeIndex
def initialize(index_uid, raise_on_failure, options)
client = MeiliSearch::Rails.client
primary_key = options[:primary_key] || MeiliSearch::Rails::IndexSettings::DEFAULT_PRIMARY_KEY
@raise_on_failure = raise_on_failure.nil? || raise_on_failure

SafeIndex.log_or_throw(nil, @raise_on_failure) do
client.create_index(index_uid, { primary_key: primary_key })
end

@index = client.index(index_uid)
end

::MeiliSearch::Index.instance_methods(false).each do |m|
define_method(m) do |*args, &block|
if m == :update_settings
args[0].delete(:attributes_to_highlight) if args[0][:attributes_to_highlight]
args[0].delete(:attributes_to_crop) if args[0][:attributes_to_crop]
args[0].delete(:crop_length) if args[0][:crop_length]
end

SafeIndex.log_or_throw(m, @raise_on_failure) do
return MeiliSearch::Rails.black_hole unless MeiliSearch::Rails.active?

@index.send(m, *args, &block)
end
end
end

# Maually define facet_search due to complications with **opts in ruby 2.*
def facet_search(*args, **opts)
SafeIndex.log_or_throw(:facet_search, @raise_on_failure) do
return MeiliSearch::Rails.black_hole unless MeiliSearch::Rails.active?

@index.facet_search(*args, **opts)
end
end

# special handling of wait_for_task to handle null task_id
def wait_for_task(task_uid)
return if task_uid.nil? && !@raise_on_failure # ok

SafeIndex.log_or_throw(:wait_for_task, @raise_on_failure) do
@index.wait_for_task(task_uid)
end
end

# special handling of settings to avoid raising errors on 404
def settings(*args)
SafeIndex.log_or_throw(:settings, @raise_on_failure) do
@index.settings(*args)
rescue ::MeiliSearch::ApiError => e
return {} if e.code == 'index_not_found' # not fatal

raise e

Check warning on line 59 in lib/meilisearch/rails/safe_index.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/safe_index.rb#L59

Added line #L59 was not covered by tests
end
end

def self.log_or_throw(method, raise_on_failure, &block)
yield
rescue ::MeiliSearch::TimeoutError, ::MeiliSearch::ApiError => e
raise e if raise_on_failure

# log the error
MeiliSearch::Rails.logger.info("[meilisearch-rails] #{e.message}")
# return something
case method.to_s
when 'search'
# some attributes are required
{ 'hits' => [], 'hitsPerPage' => 0, 'page' => 0, 'facetDistribution' => {}, 'error' => e }
else
# empty answer
{ 'error' => e }

Check warning on line 77 in lib/meilisearch/rails/safe_index.rb

View check run for this annotation

Codecov / codecov/patch

lib/meilisearch/rails/safe_index.rb#L77

Added line #L77 was not covered by tests
end
end
end
end
end
46 changes: 46 additions & 0 deletions spec/model_configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'spec_helper'
require 'support/models/unconfigured_model'
require 'support/sequel_models/book'
require 'support/models/color'

describe 'Model configuration' do
describe 'options' do
context 'if passed :per_environment' do
it 'throws error' do
expect do
UnconfiguredModel.meilisearch per_environment: true
end.to raise_error(MeiliSearch::Rails::BadConfiguration)
end
end

context 'if passed :enqueue and :synchronous' do
it 'complains about incompatible options' do
expect do
UnconfiguredModel.meilisearch enqueue: true, synchronous: true
end.to raise_error(ArgumentError)
end
end
end

describe '#sequel_model?' do
it 'returns false for activerecord' do
expect(Color.ms_config).not_to be_sequel_model
end

it 'returns true for sequel' do
expect(SequelBook.ms_config).to be_sequel_model
end

# TODO: Add similar methods for mongodb
end

describe '#active_record_model?' do
it 'returns true for activerecord' do
expect(Color.ms_config).to be_active_record_model
end

it 'returns false for sequel' do
expect(SequelBook.ms_config).not_to be_active_record_model
end
end
end
Loading
Loading