agnostic_backend 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|