Skip to content

Commit

Permalink
Merge pull request #6 from mon-territoire/refactor-multiple_contexts
Browse files Browse the repository at this point in the history
Refactor contexts and allow multiple contexts to be passed
  • Loading branch information
inkstak committed Mar 21, 2023
2 parents 78194e1 + 74e9c99 commit 3102b52
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 27 deletions.
59 changes: 57 additions & 2 deletions docs/searches/contexts.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
---
title: "[TODO] Contexts"
title: Contexts
order: -5
---
---

Contexts allows to create search scopes independently of search criteria.
Because search criteria may comes from user inputs, contexts offers a way to force search scoping :

```ruby
class ArticleSearch < Caoutsearch::Search::Base
has_context :public do
filters << { term: { published: true } }
end
end

ArticleSearch.context(:public).search(params[:q])
```

Multiple contexts can be passed together or chained:

```ruby
ArticleSearch.context(:public, :blog)
ArticleSearch.context(:public).context(:blog)
```

Current context can used to alter search queries or filters:

```ruby
class ArticleSearch < Caoutsearch::Search::Base
has_context :public do
filters << { term: { published: true } }
end

match_all do |value|
targets = %w[title body]
targets << "author" unless current_context?(:public)

filter_by(targets, value)
end
end
```

Missing contexts doesn't raise an expection and are ignored.
It allows you to apply contexts to any searches, whether the context is implemented or not:

```ruby
class BlogController < ApplicationController
def index
@articles = ArticleSearch.context(current_context).search(params[:q])
@tags = TagSearch.context(current_context).search(params[:q]) # TagSearch doesn't implement a "public" context
end

protected

def current_context
:public unless current_user&.admin?
end
end
```
2 changes: 1 addition & 1 deletion lib/caoutsearch/search/inspect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Search
module Inspect
PROPERTIES_TO_INSPECT = %i[
search_criteria
current_context
current_contexts
current_order
current_page
current_limit
Expand Down
9 changes: 7 additions & 2 deletions lib/caoutsearch/search/internal_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ module InternalDSL
# self.config[:filter][key] = ...
#
class_attribute :config, default: {
contexts: ActiveSupport::HashWithIndifferentAccess.new,
filters: ActiveSupport::HashWithIndifferentAccess.new,
defaults: ActiveSupport::HashWithIndifferentAccess.new,
suggestions: ActiveSupport::HashWithIndifferentAccess.new,
sorts: ActiveSupport::HashWithIndifferentAccess.new
}

class_attribute :contexts, instance_accessor: false, default: {}
class_attribute :aggregations, instance_accessor: false, default: {}
class_attribute :transformations, instance_accessor: false, default: {}
end
Expand All @@ -32,7 +32,7 @@ def match_all(&block)
config[:match_all] = block
end

%w[context default].each do |method|
%w[default].each do |method|
config_attribute = method.pluralize.to_sym

define_method method do |name = nil, &block|
Expand Down Expand Up @@ -68,6 +68,11 @@ def alias_sort(new_name, old_name)
sort(new_name) { |direction| sort_by(old_name, direction) }
end

def has_context(name, &block)
self.contexts = contexts.dup
contexts[name.to_s] = Caoutsearch::Search::DSL::Item.new(name, &block)
end

def has_aggregation(name, definition = {}, &block)
raise ArgumentError, "has_aggregation accepts Hash definition or block but not both" if block && definition.any?

Expand Down
10 changes: 2 additions & 8 deletions lib/caoutsearch/search/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Search
module QueryBuilder
extend ActiveSupport::Concern
include QueryBuilder::Aggregations
include QueryBuilder::Contexts

def build
reset_variable(:@elasticsearch_query)
Expand All @@ -13,7 +14,7 @@ def build
run_callbacks :build do
build_prepend_hash
build_search_criteria
build_context
build_contexts
build_defaults
build_limits
build_orders
Expand All @@ -35,13 +36,6 @@ def build_search_criteria
search_by(search_criteria)
end

def build_context
return unless current_context

item = config[:contexts][current_context.to_s]
instance_exec(&item.block) if item
end

def build_defaults
keys = search_criteria_keys.map(&:to_s)

Expand Down
34 changes: 34 additions & 0 deletions lib/caoutsearch/search/query_builder/contexts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Caoutsearch
module Search
module QueryBuilder
module Contexts
private

def build_contexts
call_contexts(*current_contexts) if current_contexts
end

def call_contexts(*args)
args.each do |arg|
call_context(arg)
end
end

def call_context(name)
name = name.to_s

if self.class.contexts.include?(name)
item = self.class.contexts[name]
call_context_item(item)
end
end

def call_context_item(item)
instance_exec(&item.block)
end
end
end
end
end
15 changes: 10 additions & 5 deletions lib/caoutsearch/search/search_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Search
module SearchMethods
extend ActiveSupport::Concern

attr_reader :current_context, :current_order, :current_aggregations,
attr_reader :current_contexts, :current_order, :current_aggregations,
:current_suggestions, :current_fields, :current_source

# Public API
Expand Down Expand Up @@ -97,8 +97,9 @@ def search!(*values)
self
end

def context!(value)
@current_context = value
def context!(*values)
@current_contexts ||= []
@current_contexts += values.flatten
self
end

Expand Down Expand Up @@ -173,7 +174,7 @@ def append!(hash)
"aggregations" => :@current_aggregations,
"suggest" => :@current_suggestions,
"suggestions" => :@current_suggestions,
"context" => :@current_context,
"context" => :@current_contexts,
"order" => :@current_order,
"page" => :@current_page,
"offset" => :@current_offset,
Expand All @@ -189,7 +190,7 @@ def unscope!(key)
self
end

# Getters
# Getters and predicates
# ------------------------------------------------------------------------
def search_criteria
@search_criteria ||= []
Expand All @@ -213,6 +214,10 @@ def current_offset
end
end

def current_context?(name)
@current_contexts&.map(&:to_s)&.include?(name.to_s)
end

# Criteria handlers
# ------------------------------------------------------------------------
def find_criterion(key)
Expand Down
60 changes: 60 additions & 0 deletions spec/caoutsearch/search/query_builder/contexts_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Caoutsearch::Search::QueryBuilder::Contexts do
let!(:search_class) do
stub_search_class("SampleSearch") do
has_context :public do
filters << {term: {published: true}}
end

has_context :blog do
filters << {term: {destination: "blog"}}
end
end
end

it "applies one context to a query" do
search = search_class.new.context(:public)

expect(search.build).to eq(
query: {
bool: {
filter: [
{term: {published: true}}
]
}
}
)
end

it "builds a query with multiple contexts" do
search = search_class.new.context(:public, :blog)

expect(search.build).to eq(
query: {
bool: {
filter: [
{term: {published: true}},
{term: {destination: "blog"}}
]
}
}
)
end

it "ignores missing contexts" do
search = search_class.new.context(:public, :api)

expect(search.build).to eq(
query: {
bool: {
filter: [
{term: {published: true}}
]
}
}
)
end
end
68 changes: 59 additions & 9 deletions spec/caoutsearch/search/search_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,71 @@
RSpec.describe Caoutsearch::Search::SearchMethods do
let!(:search_class) { stub_search_class("SampleSearch") }

it "chains search aggregations" do
search = search_class.new.aggregate(:tags).aggregate(:views)
describe "#context" do
it "registers a search context" do
search = search_class.new.context(:public)

expect(search).to have_attributes(current_aggregations: [:tags, :views])
expect(search).to have_attributes(current_contexts: [:public])
end

it "chains search contexts" do
search = search_class.new.context(:public).context(:api)

expect(search).to have_attributes(current_contexts: [:public, :api])
end

it "combines multiple contexts" do
search = search_class.new.context(:public, :api)

expect(search).to have_attributes(current_contexts: [:public, :api])
end
end

it "combines multiple aggregations" do
search = search_class.new.aggregate(:tags, :views)
describe "#context?" do
it "checks what contexts are included" do
search = search_class.new.context(:public)

aggregate_failures do
expect(search).to be_current_context(:public)
expect(search).not_to be_current_context(:blog)
end
end

it "checks a context is included, not matter it's a String or a Symbol" do
search = search_class.new.context(:public).context("api")

expect(search).to have_attributes(current_aggregations: [:tags, :views])
aggregate_failures do
expect(search).to be_current_context("public")
expect(search).to be_current_context(:api)
end
end

it "checks what contexts are included, even when no context esists" do
search = search_class.new

aggregate_failures do
expect(search).not_to be_current_context(:public)
end
end
end

it "combines multiple aggregations with arguments" do
search = search_class.new.aggregate(tags: 20)
describe "#aggregate" do
it "chains search aggregations" do
search = search_class.new.aggregate(:tags).aggregate(:views)

expect(search).to have_attributes(current_aggregations: [:tags, :views])
end

it "combines multiple aggregations" do
search = search_class.new.aggregate(:tags, :views)

expect(search).to have_attributes(current_aggregations: [:tags, :views])
end

it "combines multiple aggregations with arguments" do
search = search_class.new.aggregate(tags: 20)

expect(search).to have_attributes(current_aggregations: [{tags: 20}])
expect(search).to have_attributes(current_aggregations: [{tags: 20}])
end
end
end

0 comments on commit 3102b52

Please sign in to comment.