agnostic_backend 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +345 -0
- data/Rakefile +6 -0
- data/agnostic_backend.gemspec +34 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/doc/indexable.md +292 -0
- data/doc/queryable.md +0 -0
- data/lib/agnostic_backend/cloudsearch/index.rb +99 -0
- data/lib/agnostic_backend/cloudsearch/index_field.rb +103 -0
- data/lib/agnostic_backend/cloudsearch/indexer.rb +84 -0
- data/lib/agnostic_backend/cloudsearch/remote_index_field.rb +32 -0
- data/lib/agnostic_backend/index.rb +24 -0
- data/lib/agnostic_backend/indexable/config.rb +24 -0
- data/lib/agnostic_backend/indexable/content_manager.rb +51 -0
- data/lib/agnostic_backend/indexable/field.rb +26 -0
- data/lib/agnostic_backend/indexable/field_type.rb +48 -0
- data/lib/agnostic_backend/indexable/indexable.rb +125 -0
- data/lib/agnostic_backend/indexer.rb +48 -0
- data/lib/agnostic_backend/queryable/attribute.rb +22 -0
- data/lib/agnostic_backend/queryable/cloudsearch/executor.rb +100 -0
- data/lib/agnostic_backend/queryable/cloudsearch/query.rb +29 -0
- data/lib/agnostic_backend/queryable/cloudsearch/query_builder.rb +13 -0
- data/lib/agnostic_backend/queryable/cloudsearch/result_set.rb +27 -0
- data/lib/agnostic_backend/queryable/cloudsearch/visitor.rb +127 -0
- data/lib/agnostic_backend/queryable/criteria/binary.rb +47 -0
- data/lib/agnostic_backend/queryable/criteria/criterion.rb +13 -0
- data/lib/agnostic_backend/queryable/criteria/ternary.rb +36 -0
- data/lib/agnostic_backend/queryable/criteria_builder.rb +83 -0
- data/lib/agnostic_backend/queryable/executor.rb +43 -0
- data/lib/agnostic_backend/queryable/expressions/expression.rb +65 -0
- data/lib/agnostic_backend/queryable/operations/n_ary.rb +18 -0
- data/lib/agnostic_backend/queryable/operations/operation.rb +13 -0
- data/lib/agnostic_backend/queryable/operations/unary.rb +35 -0
- data/lib/agnostic_backend/queryable/query.rb +27 -0
- data/lib/agnostic_backend/queryable/query_builder.rb +98 -0
- data/lib/agnostic_backend/queryable/result_set.rb +42 -0
- data/lib/agnostic_backend/queryable/tree_node.rb +48 -0
- data/lib/agnostic_backend/queryable/validator.rb +174 -0
- data/lib/agnostic_backend/queryable/value.rb +26 -0
- data/lib/agnostic_backend/queryable/visitor.rb +124 -0
- data/lib/agnostic_backend/rspec/matchers.rb +69 -0
- data/lib/agnostic_backend/utilities.rb +207 -0
- data/lib/agnostic_backend/version.rb +3 -0
- data/lib/agnostic_backend.rb +49 -0
- metadata +199 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Indexable
|
3
|
+
|
4
|
+
class ContentManager
|
5
|
+
|
6
|
+
def add_definitions &block
|
7
|
+
return unless block_given?
|
8
|
+
instance_eval &block
|
9
|
+
end
|
10
|
+
|
11
|
+
def contents
|
12
|
+
@contents ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(sym, *args, **kwargs)
|
16
|
+
if FieldType.exists? sym
|
17
|
+
kwargs[:type] = sym
|
18
|
+
field(*args, **kwargs)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond_to?(sym, include_private=false)
|
25
|
+
FieldType.exists?(sym) || super
|
26
|
+
end
|
27
|
+
|
28
|
+
def field(field_name, value: nil, type:, from: nil, **options)
|
29
|
+
contents[field_name.to_s] = Field.new(value.present? ? value : field_name, type,
|
30
|
+
from: from, **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_contents_from(object, index_name)
|
34
|
+
kv_pairs = contents.map do |field_name, field|
|
35
|
+
field_value = field.evaluate(context: object)
|
36
|
+
if field.type.nested?
|
37
|
+
if field_value.respond_to? :generate_document
|
38
|
+
field_value = field_value.generate_document(for_index: index_name)
|
39
|
+
elsif field_value.present?
|
40
|
+
field_name = nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
[field_name, field_value]
|
44
|
+
end
|
45
|
+
kv_pairs.reject! { |attr_name, _| attr_name.nil? }
|
46
|
+
Hash[kv_pairs]
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Indexable
|
3
|
+
|
4
|
+
class Field
|
5
|
+
|
6
|
+
attr_accessor :value, :type, :from
|
7
|
+
|
8
|
+
def initialize(value, type, from: nil, **options)
|
9
|
+
if type == FieldType::STRUCT && from.nil?
|
10
|
+
raise "A nested type requires the specification of a target class using the `from` argument"
|
11
|
+
end
|
12
|
+
@value = value.respond_to?(:call) ? value : value.to_sym
|
13
|
+
@from = (from.is_a?(Enumerable) ? from : [from]) unless from.nil?
|
14
|
+
@type = FieldType.new(type, **options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def evaluate(context:)
|
18
|
+
value.respond_to?(:call) ?
|
19
|
+
context.instance_eval(&value) :
|
20
|
+
context.send(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Indexable
|
3
|
+
class FieldType
|
4
|
+
INTEGER = :integer
|
5
|
+
DOUBLE = :double
|
6
|
+
STRING = :string # literal string (i.e. should be matched exactly)
|
7
|
+
STRING_ARRAY = :string_array
|
8
|
+
TEXT = :text
|
9
|
+
TEXT_ARRAY = :text_array
|
10
|
+
DATE = :date # datetime
|
11
|
+
BOOLEAN = :boolean
|
12
|
+
STRUCT = :struct # a nested structure containing other values
|
13
|
+
|
14
|
+
def self.all
|
15
|
+
constants.map { |constant| const_get(constant) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.exists?(type)
|
19
|
+
all.include? type
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :type, :options
|
23
|
+
|
24
|
+
def initialize(type, **options)
|
25
|
+
raise "Type #{type} not supported" unless FieldType.exists? type
|
26
|
+
@type = type
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
|
30
|
+
def nested?
|
31
|
+
type == STRUCT
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches?(type)
|
35
|
+
self.type == type
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_option(option_name)
|
39
|
+
@options[option_name.to_sym]
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_option(option_name)
|
43
|
+
@options.has_key? option_name.to_sym
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Indexable
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_reader :includers
|
6
|
+
|
7
|
+
def indexable_class(index_name)
|
8
|
+
includers.find { |klass| klass.index_name == index_name }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(base)
|
13
|
+
@includers ||= []
|
14
|
+
@includers << base unless @includers.include? base
|
15
|
+
base.send :include, InstanceMethods
|
16
|
+
base.send :extend, ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def create_index
|
22
|
+
AgnosticBackend::Indexable::Config.create_index_for(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
# establishes the convention for determining the index name from the class name
|
26
|
+
def index_name(source=nil)
|
27
|
+
(source.nil? ? name : source.to_s).split('::').last.underscore.pluralize
|
28
|
+
end
|
29
|
+
|
30
|
+
def _index_content_managers
|
31
|
+
@__index_content_managers ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def index_content_manager(index_name)
|
35
|
+
_index_content_managers[index_name.to_s]
|
36
|
+
end
|
37
|
+
|
38
|
+
def _index_root_notifiers
|
39
|
+
@__index_root_notifiers ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def index_root_notifier(index_name)
|
43
|
+
_index_root_notifiers[index_name.to_s]
|
44
|
+
end
|
45
|
+
|
46
|
+
def schema(for_index: nil, &block)
|
47
|
+
index_name = for_index.nil? ? self.index_name : for_index
|
48
|
+
manager = index_content_manager(index_name)
|
49
|
+
raise "Index #{index_name} has not been defined for #{name}" if manager.nil?
|
50
|
+
kv_pairs = manager.contents.map do |field_name, field|
|
51
|
+
schema =
|
52
|
+
if field.type.nested?
|
53
|
+
field.from.map { |klass| klass.schema(for_index: index_name, &block) }.reduce(&:merge)
|
54
|
+
elsif block_given?
|
55
|
+
yield field.type
|
56
|
+
else
|
57
|
+
field.type.type
|
58
|
+
end
|
59
|
+
[field_name, schema]
|
60
|
+
end
|
61
|
+
Hash[kv_pairs]
|
62
|
+
end
|
63
|
+
|
64
|
+
# specifies which fields should be indexed for a given index_name
|
65
|
+
# also sets up the manager for the specified index_name
|
66
|
+
def define_index_fields(owner: nil, &block)
|
67
|
+
return unless block_given?
|
68
|
+
_index_content_managers[index_name(owner)] ||= ContentManager.new
|
69
|
+
_index_content_managers[index_name(owner)].add_definitions &block
|
70
|
+
unless instance_methods(false).include? :_index_content_managers
|
71
|
+
define_method(:_index_content_managers) { self.class._index_content_managers }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# specifies who should be notified when this object is saved
|
76
|
+
def define_index_notifier(target: nil, &block)
|
77
|
+
return unless block_given?
|
78
|
+
_index_root_notifiers[index_name(target)] = block
|
79
|
+
unless instance_methods(false).include? :_index_root_notifiers
|
80
|
+
define_method(:_index_root_notifiers) { self.class._index_root_notifiers }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module InstanceMethods
|
86
|
+
|
87
|
+
def index_name(source=nil)
|
88
|
+
self.class.index_name(source)
|
89
|
+
end
|
90
|
+
|
91
|
+
def generate_document(for_index: nil)
|
92
|
+
index_name = for_index.nil? ? self.index_name : for_index.to_s
|
93
|
+
return unless respond_to? :_index_content_managers
|
94
|
+
manager = _index_content_managers[index_name.to_s]
|
95
|
+
raise "Index #{index_name} does not exist" if manager.nil?
|
96
|
+
manager.extract_contents_from self, index_name
|
97
|
+
end
|
98
|
+
|
99
|
+
def put_to_index(index_name=nil)
|
100
|
+
indexable_class = index_name.nil? ?
|
101
|
+
self.class :
|
102
|
+
AgnosticBackend::Indexable.indexable_class(index_name)
|
103
|
+
|
104
|
+
index = indexable_class.create_index
|
105
|
+
indexer = index.indexer
|
106
|
+
indexer.put(self)
|
107
|
+
end
|
108
|
+
|
109
|
+
def index_object(index_name)
|
110
|
+
put_to_index(index_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def trigger_index_notification
|
116
|
+
return unless respond_to? :_index_root_notifiers
|
117
|
+
_index_root_notifiers.each do |index_name, block|
|
118
|
+
obj = instance_eval &block
|
119
|
+
obj = [obj] unless obj.is_a? Enumerable
|
120
|
+
obj.each { |o| o.index_object(index_name) if o.present? }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
class Indexer
|
3
|
+
|
4
|
+
attr_reader :index
|
5
|
+
|
6
|
+
def initialize(index)
|
7
|
+
@index = index
|
8
|
+
end
|
9
|
+
|
10
|
+
# Sends the specified document to the remote backend.
|
11
|
+
# This is a template method.
|
12
|
+
# @param [Indexable] an Indexable object
|
13
|
+
# @returns [boolean] true if success, false if failure
|
14
|
+
# returns nil if no indexing attempt is made (e.g. generated document is empty)
|
15
|
+
def put(indexable)
|
16
|
+
document = indexable.generate_document
|
17
|
+
return if document.blank?
|
18
|
+
begin
|
19
|
+
publish(transform(prepare(document)))
|
20
|
+
true
|
21
|
+
rescue => e
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Deletes the specified document from the index, This is an abstract
|
27
|
+
# method which concrete index classes must implement in order to provide
|
28
|
+
# its functionality.
|
29
|
+
# @param [document_id] the document id of the indexed document
|
30
|
+
def delete(document_id)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def publish(document)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def transform(document)
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def prepare(document)
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
class Attribute < TreeNode
|
4
|
+
include AgnosticBackend::Utilities
|
5
|
+
|
6
|
+
attr_reader :name, :parent
|
7
|
+
|
8
|
+
def initialize(name, parent:, context:)
|
9
|
+
super([], context)
|
10
|
+
@name, @parent = name, parent
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(o)
|
14
|
+
super && o.name == name
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
value_for_key(context.index.schema, name).try(:type)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module AgnosticBackend
|
4
|
+
module Queryable
|
5
|
+
module Cloudsearch
|
6
|
+
class Executor < AgnosticBackend::Queryable::Executor
|
7
|
+
include AgnosticBackend::Utilities
|
8
|
+
|
9
|
+
def execute
|
10
|
+
with_exponential_backoff Aws::CloudSearch::Errors::Throttling do
|
11
|
+
response = client.search(params)
|
12
|
+
ResultSet.new(response, query)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
result = ''
|
18
|
+
result += "search?q=#{query_expression}" if query_expression
|
19
|
+
result += " return=#{return_expression}" if return_expression
|
20
|
+
result += " sort=#{sort}" if sort
|
21
|
+
result += " size=#{size}" if size
|
22
|
+
result += " offset=#{start}" if start
|
23
|
+
result += " cursor=#{scroll_cursor}" if scroll_cursor
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def params
|
28
|
+
{
|
29
|
+
cursor: scroll_cursor,
|
30
|
+
expr: expr,
|
31
|
+
facet: facet,
|
32
|
+
filter_query: filter_query,
|
33
|
+
highlight: highlight,
|
34
|
+
partial: partial,
|
35
|
+
query: query_expression,
|
36
|
+
query_options: query_options,
|
37
|
+
query_parser: query_parser,
|
38
|
+
return: return_expression,
|
39
|
+
size: size,
|
40
|
+
sort: sort,
|
41
|
+
start: start
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def client
|
48
|
+
query.context.index.cloudsearch_domain_client
|
49
|
+
end
|
50
|
+
|
51
|
+
def filter_query
|
52
|
+
end
|
53
|
+
|
54
|
+
def query_expression
|
55
|
+
where_expression ? where_expression.accept(visitor) : 'matchall'
|
56
|
+
end
|
57
|
+
|
58
|
+
def scroll_cursor
|
59
|
+
scroll_cursor_expression.accept(visitor) if scroll_cursor_expression
|
60
|
+
end
|
61
|
+
|
62
|
+
def start
|
63
|
+
offset_expression.accept(visitor) if offset_expression
|
64
|
+
end
|
65
|
+
|
66
|
+
def expr
|
67
|
+
end
|
68
|
+
|
69
|
+
def facet
|
70
|
+
end
|
71
|
+
|
72
|
+
def highlight
|
73
|
+
end
|
74
|
+
|
75
|
+
def partial
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def query_options
|
80
|
+
end
|
81
|
+
|
82
|
+
def query_parser
|
83
|
+
'structured'
|
84
|
+
end
|
85
|
+
|
86
|
+
def return_expression
|
87
|
+
select_expression.accept(visitor) if select_expression
|
88
|
+
end
|
89
|
+
|
90
|
+
def size
|
91
|
+
limit_expression.accept(visitor) if limit_expression
|
92
|
+
end
|
93
|
+
|
94
|
+
def sort
|
95
|
+
order_expression.accept(visitor) if order_expression
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
module Cloudsearch
|
4
|
+
class Query < AgnosticBackend::Queryable::Query
|
5
|
+
|
6
|
+
def initialize(base)
|
7
|
+
super
|
8
|
+
@executor = Executor.new(self, Visitor.new)
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
@executor.execute if valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute!
|
16
|
+
if valid?
|
17
|
+
@executor.execute
|
18
|
+
else
|
19
|
+
raise StandardError, errors
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
@executor.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
module Cloudsearch
|
4
|
+
class QueryBuilder < AgnosticBackend::Queryable::QueryBuilder
|
5
|
+
private
|
6
|
+
|
7
|
+
def create_query(context)
|
8
|
+
AgnosticBackend::Queryable::Cloudsearch::Query.new(context)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
module Cloudsearch
|
4
|
+
class ResultSet < AgnosticBackend::Queryable::ResultSet
|
5
|
+
include AgnosticBackend::Utilities
|
6
|
+
|
7
|
+
def total_count
|
8
|
+
raw_results.hits.found
|
9
|
+
end
|
10
|
+
|
11
|
+
def cursor
|
12
|
+
raw_results.hits.cursor
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def filtered_results
|
18
|
+
raw_results.hits.hit.map(&:fields)
|
19
|
+
end
|
20
|
+
|
21
|
+
def transform(result)
|
22
|
+
transform_nested_values(unflatten(result), Proc.new{|value| value.size > 1 ? value.split.join('|') : value.first})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
module Cloudsearch
|
4
|
+
class Visitor < AgnosticBackend::Queryable::Visitor
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def visit_criteria_equal(subject)
|
9
|
+
"(term field=#{visit(subject.attribute)} #{visit(subject.value)})"
|
10
|
+
end
|
11
|
+
|
12
|
+
def visit_criteria_not_equal(subject)
|
13
|
+
"(not term field=#{visit(subject.attribute)} #{visit(subject.value)})"
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_criteria_greater(subject)
|
17
|
+
"(range field=#{visit(subject.attribute)} {#{visit(subject.value)},})"
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_criteria_less(subject)
|
21
|
+
"(range field=#{visit(subject.attribute)} {,#{visit(subject.value)}})"
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_criteria_greater_equal(subject)
|
25
|
+
"(range field=#{visit(subject.attribute)} [#{visit(subject.value)},})"
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_criteria_less_equal(subject)
|
29
|
+
"(range field=#{visit(subject.attribute)} {,#{visit(subject.value)}])"
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_criteria_greater_and_less(subject)
|
33
|
+
"(range field=#{visit(subject.attribute)} {#{visit(subject.left_value)},#{visit(subject.right_value)}})"
|
34
|
+
end
|
35
|
+
|
36
|
+
def visit_criteria_greater_equal_and_less(subject)
|
37
|
+
"(range field=#{visit(subject.attribute)} [#{visit(subject.left_value)},#{visit(subject.right_value)}})"
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_criteria_greater_and_less_equal(subject)
|
41
|
+
"(range field=#{visit(subject.attribute)} {#{visit(subject.left_value)},#{visit(subject.right_value)}])"
|
42
|
+
end
|
43
|
+
|
44
|
+
def visit_criteria_greater_equal_and_less_equal(subject)
|
45
|
+
"(range field=#{visit(subject.attribute)} [#{visit(subject.left_value)},#{visit(subject.right_value)}])"
|
46
|
+
end
|
47
|
+
|
48
|
+
def visit_criteria_contains(subject)
|
49
|
+
"(phrase field=#{visit(subject.attribute)} #{visit(subject.value)})"
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit_criteria_starts(subject)
|
53
|
+
"(prefix field=#{visit(subject.attribute)} #{visit(subject.value)})"
|
54
|
+
end
|
55
|
+
|
56
|
+
def visit_operations_not(subject)
|
57
|
+
"(not #{visit(subject.operand)})"
|
58
|
+
end
|
59
|
+
|
60
|
+
def visit_operations_and(subject)
|
61
|
+
"(and #{subject.operands.map{|o| visit(o)}.join(' ')})"
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_operations_or(subject)
|
65
|
+
"(or #{subject.operands.map{|o| visit(o)}.join(' ')})"
|
66
|
+
end
|
67
|
+
|
68
|
+
def visit_operations_ascending(subject)
|
69
|
+
"#{visit(subject.attribute)} asc"
|
70
|
+
end
|
71
|
+
|
72
|
+
def visit_operations_descending(subject)
|
73
|
+
"#{visit(subject.attribute)} desc"
|
74
|
+
end
|
75
|
+
|
76
|
+
def visit_query(subject)
|
77
|
+
"#{subject.children.map{|c| visit(c)}.join(' ')}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def visit_expressions_where(subject)
|
81
|
+
visit(subject.criterion) #search?q=
|
82
|
+
end
|
83
|
+
|
84
|
+
def visit_expressions_select(subject)
|
85
|
+
"#{subject.projections.map{|c| visit(c)}.join(',')}" #return=
|
86
|
+
end
|
87
|
+
|
88
|
+
def visit_expressions_order(subject)
|
89
|
+
"#{subject.qualifiers.map{|c| visit(c)}.join(',')}" #sort=
|
90
|
+
end
|
91
|
+
|
92
|
+
def visit_expressions_limit(subject)
|
93
|
+
visit(subject.limit) #size=
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_expressions_offset(subject)
|
97
|
+
visit(subject.offset) #offset=
|
98
|
+
end
|
99
|
+
|
100
|
+
def visit_expressions_scroll_cursor(subject)
|
101
|
+
visit(subject.scroll_cursor) #cursor=
|
102
|
+
end
|
103
|
+
|
104
|
+
def visit_attribute(subject)
|
105
|
+
subject.name.split('.').join('__')
|
106
|
+
end
|
107
|
+
|
108
|
+
def visit_value(subject)
|
109
|
+
case subject.type
|
110
|
+
when :integer
|
111
|
+
subject.value
|
112
|
+
when :date
|
113
|
+
"'#{subject.value.utc.strftime("%Y-%m-%dT%H:%M:%SZ")}'"
|
114
|
+
when :double
|
115
|
+
subject.value
|
116
|
+
when :boolean
|
117
|
+
"'#{subject.value}'"
|
118
|
+
when :string,:string_array,:text,:text_array
|
119
|
+
"'#{subject.value}'"
|
120
|
+
else
|
121
|
+
subject.value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AgnosticBackend
|
2
|
+
module Queryable
|
3
|
+
module Criteria
|
4
|
+
class Binary < Criterion
|
5
|
+
attr_reader :attribute, :value
|
6
|
+
|
7
|
+
def initialize(attribute:, value:, context: nil)
|
8
|
+
@attribute, @value = attribute, value
|
9
|
+
super([attribute, value], context)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Relational < Binary
|
14
|
+
|
15
|
+
def initialize(attribute:, value:, context: nil)
|
16
|
+
attribute = attribute_component(attribute: attribute, context: context)
|
17
|
+
value = value_component(value: value, context: context, type: attribute.type)
|
18
|
+
super(attribute: attribute, value: value, context: context)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Equal < Relational;
|
23
|
+
end
|
24
|
+
|
25
|
+
class NotEqual < Relational;
|
26
|
+
end
|
27
|
+
|
28
|
+
class Greater < Relational;
|
29
|
+
end
|
30
|
+
|
31
|
+
class Less < Relational;
|
32
|
+
end
|
33
|
+
|
34
|
+
class GreaterEqual < Relational;
|
35
|
+
end
|
36
|
+
|
37
|
+
class LessEqual < Relational;
|
38
|
+
end
|
39
|
+
|
40
|
+
class Contains < Relational;
|
41
|
+
end
|
42
|
+
|
43
|
+
class Starts < Relational;
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|