stretchy-model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +146 -0
  9. data/Rakefile +4 -0
  10. data/containers/Dockerfile.elasticsearch +7 -0
  11. data/containers/Dockerfile.opensearch +19 -0
  12. data/docker-compose.yml +52 -0
  13. data/lib/active_model/type/array.rb +13 -0
  14. data/lib/active_model/type/hash.rb +15 -0
  15. data/lib/rails/instrumentation/publishers.rb +29 -0
  16. data/lib/rails/instrumentation/railtie.rb +29 -0
  17. data/lib/stretchy/associations/associated_validator.rb +17 -0
  18. data/lib/stretchy/associations/elastic_relation.rb +38 -0
  19. data/lib/stretchy/associations.rb +161 -0
  20. data/lib/stretchy/common.rb +33 -0
  21. data/lib/stretchy/delegation/delegate_cache.rb +131 -0
  22. data/lib/stretchy/delegation/gateway_delegation.rb +43 -0
  23. data/lib/stretchy/indexing/bulk.rb +48 -0
  24. data/lib/stretchy/model/callbacks.rb +31 -0
  25. data/lib/stretchy/model/serialization.rb +20 -0
  26. data/lib/stretchy/null_relation.rb +53 -0
  27. data/lib/stretchy/persistence.rb +43 -0
  28. data/lib/stretchy/querying.rb +20 -0
  29. data/lib/stretchy/record.rb +57 -0
  30. data/lib/stretchy/refreshable.rb +15 -0
  31. data/lib/stretchy/relation.rb +169 -0
  32. data/lib/stretchy/relations/finder_methods.rb +39 -0
  33. data/lib/stretchy/relations/merger.rb +179 -0
  34. data/lib/stretchy/relations/query_builder.rb +265 -0
  35. data/lib/stretchy/relations/query_methods.rb +578 -0
  36. data/lib/stretchy/relations/search_option_methods.rb +34 -0
  37. data/lib/stretchy/relations/spawn_methods.rb +60 -0
  38. data/lib/stretchy/repository.rb +10 -0
  39. data/lib/stretchy/scoping/default.rb +134 -0
  40. data/lib/stretchy/scoping/named.rb +68 -0
  41. data/lib/stretchy/scoping/scope_registry.rb +34 -0
  42. data/lib/stretchy/scoping.rb +28 -0
  43. data/lib/stretchy/shared_scopes.rb +34 -0
  44. data/lib/stretchy/utils.rb +69 -0
  45. data/lib/stretchy/version.rb +5 -0
  46. data/lib/stretchy.rb +38 -0
  47. data/sig/stretchy.rbs +4 -0
  48. data/stretchy.logo.png +0 -0
  49. metadata +247 -0
@@ -0,0 +1,39 @@
1
+ module Stretchy
2
+ module Relations
3
+
4
+ module FinderMethods
5
+
6
+ def first
7
+ return results.first if @loaded
8
+ spawn.first!.results.first
9
+ end
10
+
11
+ def first!
12
+ spawn.sort(Hash[default_sort_key, :asc]).spawn.size(1)
13
+ self
14
+ end
15
+
16
+ def last
17
+ return results.last if @loaded
18
+ spawn.last!.results.first
19
+ end
20
+
21
+ def last!
22
+ spawn.sort(Hash[default_sort_key, :desc]).spawn.size(1)
23
+ self
24
+ end
25
+
26
+ def count
27
+ return results.count if @loaded
28
+ spawn.count!
29
+ end
30
+
31
+ def count!
32
+ @values[:count] = true
33
+ @values.delete(:size)
34
+ spawn.results
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,179 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require "set"
3
+
4
+ module Stretchy
5
+ module Relations
6
+ class Relation
7
+ class HashMerger # :nodoc:
8
+ attr_reader :relation, :hash
9
+
10
+ def initialize(relation, hash)
11
+ hash.assert_valid_keys(*Relation::VALUE_METHODS)
12
+
13
+ @relation = relation
14
+ @hash = hash
15
+ end
16
+
17
+ def merge
18
+ Merger.new(relation, other).merge
19
+ end
20
+
21
+ # Applying values to a relation has some side effects. E.g.
22
+ # interpolation might take place for where values. So we should
23
+ # build a relation to merge in rather than directly merging
24
+ # the values.
25
+ def other
26
+ other = Relation.create(relation.klass)
27
+ hash.each { |k, v|
28
+ if k == :joins
29
+ if Hash === v
30
+ other.joins!(v)
31
+ else
32
+ other.joins!(*v)
33
+ end
34
+ elsif k == :select
35
+ other._select!(v)
36
+ else
37
+ other.send("#{k}!", v)
38
+ end
39
+ }
40
+ other
41
+ end
42
+ end
43
+
44
+ class Merger # :nodoc:
45
+ attr_reader :relation, :values, :other
46
+
47
+ def initialize(relation, other)
48
+ @relation = relation
49
+ @values = other.values
50
+ @other = other
51
+ end
52
+
53
+ NORMAL_VALUES = [:where, :first, :last, :filter]
54
+
55
+ def normal_values
56
+ NORMAL_VALUES
57
+ end
58
+
59
+ def merge
60
+ normal_values.each do |name|
61
+ value = values[name]
62
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
63
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
64
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
65
+ # don't fall through the cracks.
66
+
67
+ unless value.nil? || (value.blank? && false != value)
68
+ if name == :select
69
+ relation._select!(*value)
70
+ elsif name == :filter
71
+ values.each do |v|
72
+ relation.send("#{name}!", v.first, v.last)
73
+ end
74
+ else
75
+ relation.send("#{name}!", *value)
76
+ end
77
+ end
78
+ end
79
+
80
+ merge_multi_values
81
+ merge_single_values
82
+ #merge_joins
83
+
84
+ relation
85
+ end
86
+
87
+ private
88
+
89
+ def merge_joins
90
+ return if values[:joins].blank?
91
+
92
+ if other.klass == relation.klass
93
+ relation.joins!(*values[:joins])
94
+ else
95
+ joins_dependency, rest = values[:joins].partition do |join|
96
+ case join
97
+ when Hash, Symbol, Array
98
+ true
99
+ else
100
+ false
101
+ end
102
+ end
103
+
104
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
105
+ joins_dependency,
106
+ [])
107
+ relation.joins! rest
108
+
109
+ @relation = relation.joins join_dependency
110
+ end
111
+ end
112
+
113
+ def merge_multi_values
114
+ lhs_wheres = relation.where_values
115
+ rhs_wheres = values[:where] || []
116
+
117
+ lhs_filters = relation.filter_values
118
+ rhs_filters = values[:filter] || []
119
+
120
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
121
+
122
+ where_values = kept + rhs_wheres
123
+
124
+ filters_removed, filters_kept = partition_overwrites(lhs_wheres, rhs_wheres)
125
+ filter_values = rhs_filters
126
+
127
+
128
+ relation.where_values = where_values.empty? ? nil : where_values
129
+ relation.filter_values = filter_values.empty? ? nil : filter_values
130
+
131
+ if values[:reordering]
132
+ # override any order specified in the original relation
133
+ relation.reorder! values[:order]
134
+ elsif values[:order]
135
+ # merge in order_values from relation
136
+ relation.order! values[:order]
137
+ end
138
+
139
+ relation.extend(*values[:extending]) unless values[:extending].blank?
140
+ end
141
+
142
+ def merge_single_values
143
+ #relation.from_value = values[:from] unless relation.from_value
144
+ #relation.lock_value = values[:lock] unless relation.lock_value
145
+
146
+ unless values[:create_with].blank?
147
+ relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
148
+ end
149
+ end
150
+
151
+ def filter_binds(lhs_binds, removed_wheres)
152
+ return lhs_binds if removed_wheres.empty?
153
+
154
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
155
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
156
+ end
157
+
158
+ # Remove equalities from the existing relation with a LHS which is
159
+ # present in the relation being merged in.
160
+ # returns [things_to_remove, things_to_keep]
161
+ def partition_overwrites(lhs_wheres, rhs_wheres)
162
+ if lhs_wheres.empty? || rhs_wheres.empty?
163
+ return [[], lhs_wheres]
164
+ end
165
+
166
+ nodes = rhs_wheres.find_all do |w|
167
+ w.respond_to?(:operator) && w.operator == :==
168
+ end
169
+ seen = Set.new(nodes) { |node| node.left }
170
+
171
+ lhs_wheres.partition do |w|
172
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
@@ -0,0 +1,265 @@
1
+ module Stretchy
2
+ module Relations
3
+ class QueryBuilder
4
+
5
+ attr_reader :structure, :values
6
+
7
+ def initialize(values)
8
+ @structure = Jbuilder.new ignore_nil: true
9
+ @values = values
10
+ end
11
+
12
+ def aggregations
13
+ values[:aggregation]
14
+ end
15
+
16
+ def filters
17
+ values[:filter]
18
+ end
19
+
20
+ def or_filters
21
+ values[:or_filter]
22
+ end
23
+
24
+ def query
25
+ @query ||= compact_where(values[:where])
26
+ end
27
+
28
+ def query_strings
29
+ @query_string ||= compact_where(values[:query_string], bool: false)
30
+ end
31
+
32
+ def must_nots
33
+ @must_nots ||= compact_where(values[:must_not])
34
+ end
35
+
36
+ def shoulds
37
+ @shoulds ||= compact_where(values[:should])
38
+ end
39
+
40
+ def fields
41
+ values[:field]
42
+ end
43
+
44
+ def source
45
+ values[:source]
46
+ end
47
+
48
+ def highlights
49
+ values[:highlight]
50
+ end
51
+
52
+ def size
53
+ values[:size]
54
+ end
55
+
56
+ def sort
57
+ values[:order]
58
+ end
59
+
60
+ def query_filters
61
+ values[:filter]
62
+ end
63
+
64
+ def search_options
65
+ build_search_options
66
+ end
67
+
68
+ def query_string_options
69
+ @query_string_options || {}
70
+ end
71
+
72
+ def count?
73
+ values[:count]
74
+ end
75
+
76
+ def to_elastic
77
+ @structure = Jbuilder.new ignore_nil: true
78
+ build_query
79
+ build_sort unless sort.blank?
80
+ build_highlights unless highlights.blank?
81
+ build_fields unless fields.blank?
82
+ build_source unless source.blank?
83
+ build_aggregations unless aggregations.blank?
84
+ structure.attributes!.with_indifferent_access
85
+ end
86
+
87
+ private
88
+
89
+ def missing_bool_query?
90
+ query.nil? && must_nots.nil? && shoulds.nil?
91
+ end
92
+
93
+ def missing_query_string?
94
+ query_strings.nil?
95
+ end
96
+
97
+ def missing_query_filter?
98
+ query_filters.nil? && or_filters.nil?
99
+ end
100
+
101
+ def build_query
102
+ return if missing_bool_query? && missing_query_string? && missing_query_filter?
103
+ structure.query do
104
+ structure.bool do
105
+ structure.must query unless missing_bool_query?
106
+ structure.must_not must_nots unless must_nots.nil?
107
+ structure.set! :should, shoulds unless shoulds.nil?
108
+
109
+ build_filtered_query if query_filters || or_filters
110
+
111
+ end unless missing_bool_query? && missing_query_filter?
112
+
113
+
114
+
115
+ structure.query_string do
116
+ structure.extract! query_string_options, *query_string_options.keys
117
+ structure.query query_strings
118
+ end unless query_strings.nil?
119
+ end.with_indifferent_access
120
+ end
121
+
122
+ def build_filtered_query
123
+ structure.filter do
124
+ structure.or do
125
+ or_filters.each do |f|
126
+ structure.child! do
127
+ structure.set! f[:name], extract_filters(f[:name], f[:args])
128
+ end
129
+ end
130
+ end unless or_filters.blank?
131
+
132
+ query_filters.each do |f|
133
+ structure.child! do
134
+ structure.set! f[:name], extract_filters(f[:name], f[:args])
135
+ end
136
+ end unless query_filters.blank?
137
+ end
138
+ end
139
+
140
+ def build_source
141
+ if [true,false].include? source.first
142
+ structure._source source.first
143
+ else
144
+ structure._source do
145
+ structure.includes source.first.delete(:includes) if source.first.has_key? :includes
146
+ structure.excludes source.first.delete(:excludes) if source.first.has_key? :excludes
147
+ end
148
+ end
149
+ end
150
+
151
+ def build_sort
152
+ structure.sort sort.flatten #.inject(Hash.new) { |h,v| h.merge(v) }
153
+ end
154
+
155
+ def build_highlights
156
+ structure.highlight do
157
+ structure.fields do
158
+ highlights.each do |highlight|
159
+ structure.set! highlight, extract_highlighter(highlight)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def build_aggregations
166
+ structure.aggregations do
167
+ aggregations.each do |agg|
168
+ structure.set! agg[:name], aggregation(agg[:name], agg[:args])
169
+ end
170
+ end
171
+ end
172
+
173
+ def build_fields
174
+ structure.fields do
175
+ structure.array! fields.flatten
176
+ end
177
+ end
178
+
179
+ def build_search_options
180
+ values[:search_option] ||= []
181
+
182
+ opts = extra_search_options
183
+ (values[:search_option] + [opts]).compact.inject(Hash.new) { |h,k,v| h.merge(k) }
184
+ end
185
+
186
+ def extra_search_options
187
+ [:size].inject(Hash.new) { |h,k| h[k] = self.send(k) unless self.send(k).nil?; h}
188
+ end
189
+
190
+ def compact_where(q, opts = {bool:true})
191
+ return if q.nil?
192
+ if opts.delete(:bool)
193
+ as_must(q)
194
+ else
195
+ as_query_string(q.flatten)
196
+ end
197
+ end
198
+
199
+ def as_must(q)
200
+ _must = []
201
+ q.each do |arg|
202
+ arg.each_pair { |k,v| _must << (v.is_a?(Array) ? {terms: Hash[k,v]} : {term: Hash[k,v]}) } if arg.class == Hash
203
+ _must << {term: Hash[[arg.split(/:/).collect(&:strip)]]} if arg.class == String
204
+ _must << arg.first if arg.class == Array
205
+ end
206
+ _must.length == 1 ? _must.first : _must
207
+ end
208
+
209
+ def as_query_string(q)
210
+ _and = []
211
+
212
+ @query_string_options = q.pop if q.length > 1
213
+
214
+ q.each do |arg|
215
+ arg.each_pair { |k,v| _and << "(#{k}:#{v})" } if arg.class == Hash
216
+ _and << "(#{arg})" if arg.class == String
217
+ end
218
+ _and.join(" AND ")
219
+ end
220
+
221
+
222
+
223
+ def extract_highlighter(highlighter)
224
+ Jbuilder.new do |highlight|
225
+ highlight.extract! highlighter
226
+ end
227
+ end
228
+
229
+ def extract_filters(name,opts = {})
230
+ Jbuilder.new do |filter|
231
+ case
232
+ when opts.is_a?(Hash)
233
+ filter.extract! opts, *opts.keys
234
+ when opts.is_a?(Array)
235
+ extract_filter_arguments_from_array(filter, opts)
236
+ else
237
+ raise "#filter only accepts Hash or Array"
238
+ end
239
+ end
240
+ end
241
+
242
+ def aggregation(name, opts = {})
243
+ Jbuilder.new do |agg|
244
+ case
245
+ when opts.is_a?(Hash)
246
+ agg.extract! opts, *opts.keys
247
+ when opts.is_a?(Array)
248
+ extract_filter_arguments_from_array(agg, opts)
249
+ else
250
+ raise "#aggregation only accepts Hash or Array"
251
+ end
252
+ end
253
+ end
254
+
255
+ def extract_filter_arguments_from_array(element, opts)
256
+ opts.each do |opt|
257
+ element.child! do
258
+ element.extract! opt , *opt.keys
259
+ end
260
+ end
261
+ end
262
+
263
+ end
264
+ end
265
+ end