lycra 0.0.7 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,143 @@
1
+ module Lycra
2
+ module Attributes
3
+ class Attribute
4
+ attr_reader :resolved, :required, :klass
5
+
6
+ def initialize(name=nil, type=nil, *args, **opts, &block)
7
+ @name = name
8
+ @name ||= opts[:name]
9
+
10
+ @nested_type = type.is_a?(Array)
11
+ @type = [type].flatten.compact.first
12
+ @type ||= [opts[:type]].flatten.compact.first
13
+ if @type.ancestors.include?(Lycra::Attributes)
14
+ @type = Lycra::Types.custom(@type)
15
+ end
16
+
17
+ @klass = opts[:klass]
18
+
19
+ @mappings = opts[:mappings] || opts[:mapping]
20
+
21
+ @resolver = args.find { |arg| arg.is_a?(Proc) || arg.is_a?(Symbol) }
22
+ @resolver = opts[:resolve] if opts.key?(:resolve)
23
+ @resolver = opts[:resolver] if opts.key?(:resolver)
24
+
25
+ @description = args.find { |arg| arg.is_a?(String) }
26
+ @description = opts[:description] if opts.key?(:description)
27
+
28
+ @required = opts[:required] || false
29
+ @cache = opts[:cache] || false
30
+
31
+ instance_exec &block if block_given?
32
+ end
33
+
34
+ def name(name=nil)
35
+ @name = name if name
36
+ @name
37
+ end
38
+
39
+ def type(type=nil)
40
+ if type
41
+ @nested_type = type.is_a?(Array)
42
+ @type = [type].flatten.compact.first
43
+ if @type.ancestors.include?(Lycra::Attributes)
44
+ @type = Lycra::Types.custom(@type)
45
+ end
46
+ end
47
+ @type
48
+ end
49
+
50
+ def nested?
51
+ !!@nested_type
52
+ end
53
+
54
+ def mappings(mappings=nil)
55
+ @mappings = mappings if mappings
56
+
57
+ map_type = type.type
58
+ if map_type.is_a?(Class) && map_type.ancestors.include?(Lycra::Attributes)
59
+ map_type = @nested_type ? "nested" : "object"
60
+ end
61
+
62
+ {type: map_type}.merge(@mappings || {})
63
+ end
64
+ alias_method :mapping, :mappings
65
+
66
+ def description(description=nil)
67
+ @description = description if description
68
+ @description
69
+ end
70
+
71
+ def resolver
72
+ @resolver ||= name.to_sym
73
+ end
74
+
75
+ def required!
76
+ @required = true
77
+ end
78
+
79
+ def required?
80
+ !!@required
81
+ end
82
+
83
+ def resolve!(document, *args, **ctxt)
84
+ @resolved ||= begin
85
+ # TODO wrap this whole block in cache if caching is enabled
86
+ if resolver.is_a?(Proc)
87
+ result = resolver.call(document.subject, args, ctxt)
88
+ elsif resolver.is_a?(Symbol)
89
+ if document.methods.include?(resolver)
90
+ result = document.send(resolver)
91
+ else
92
+ result = document.subject.send(resolver)
93
+ end
94
+ end
95
+
96
+ rslvd = type.new(result)
97
+
98
+ unless rslvd.valid?(required?, nested?)
99
+ rslvd_type = rslvd.type
100
+ rslvd_type = "array[#{rslvd.type}]" if nested?
101
+ raise Lycra::AttributeError,
102
+ "Invalid value #{rslvd.value} (#{rslvd.value.class.name}) " +
103
+ "for type '#{rslvd_type}' in field #{name} on #{document}"
104
+ end
105
+
106
+ rslvd.transform
107
+ end
108
+ end
109
+
110
+ def resolved?
111
+ instance_variable_defined? :@resolved
112
+ end
113
+
114
+ def reload
115
+ remove_instance_variable :@resolved
116
+ self
117
+ end
118
+
119
+ def as_json(options={})
120
+ {
121
+ name: name,
122
+ type: type.type,
123
+ required: required,
124
+ description: description,
125
+ mappings: mappings,
126
+ resolver: resolver.is_a?(Symbol) ? resolver : resolver.to_s
127
+ }
128
+ end
129
+
130
+ private
131
+
132
+ def resolve(resolver=nil, &block)
133
+ @resolver = resolver if resolver
134
+ @resolver = block if block_given?
135
+ @resolver
136
+ end
137
+
138
+ def types
139
+ Lycra::Types
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,38 @@
1
+ module Lycra
2
+ module Attributes
3
+ class Collection
4
+ include Enumerable
5
+
6
+ attr_reader :attributes
7
+
8
+ def initialize(klass, attributes={})
9
+ @klass = klass
10
+ @attributes = attributes
11
+ end
12
+
13
+ def dup(klass=nil)
14
+ self.class.new(klass || @klass, attributes.map { |k,attr|
15
+ duped = attr.dup
16
+ duped.instance_variable_set(:@klass, klass || @klass)
17
+ [k, duped]
18
+ }.to_h)
19
+ end
20
+
21
+ def each(&block)
22
+ @attributes.each(&block)
23
+ end
24
+
25
+ def method_missing(meth, *args, &block)
26
+ if @attributes.respond_to?(meth)
27
+ @attributes.send(meth, *args, &block)
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def respond_to_missing?(meth, include_private=false)
34
+ @attributes.respond_to?(meth, include_private) || super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ if defined?(AwesomePrint)
2
+ require 'awesome_print/formatters/lycra_attribute_formatter'
3
+ require 'awesome_print/formatters/lycra_attributes_formatter'
4
+ require 'awesome_print/ext/lycra_attribute'
5
+ require 'awesome_print/ext/lycra_attributes'
6
+ end
@@ -0,0 +1,12 @@
1
+ module Lycra
2
+ module Decorator
3
+ def self.included(base)
4
+ base.send :include, Attributes
5
+ base.send :extend, Inheritance
6
+ end
7
+
8
+ def as_json(options={})
9
+ resolve!(subject).as_json(options)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ module Lycra
2
+ module Decorator
3
+ module Model
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def decorator(klass=nil)
11
+ @_lycra_decorator = klass if klass
12
+ @_lycra_decorator || ("#{name}Decorator".constantize rescue nil)
13
+ end
14
+
15
+ def decorator=(klass)
16
+ decorator klass
17
+ end
18
+ end
19
+
20
+ module InstanceMethods
21
+ def reload
22
+ @decorator = nil
23
+ super
24
+ end
25
+
26
+ def decorator(decorator_class=nil)
27
+ return decorator_class.new(self) if decorator_class
28
+ @decorator ||= self.class.decorator.new(self)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ require 'lycra/document/proxy'
2
+
3
+ module Lycra
4
+ module Document
5
+ def self.included(base)
6
+ base.send :include, Attributes
7
+ base.send :extend, Inheritance
8
+ base.send :include, Proxy
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ module Lycra
2
+ module Document
3
+ module Model
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ delegate :__lycra__, :index_name, :document_type, :import, :search, to: :document
11
+
12
+ def document(klass=nil)
13
+ @_lycra_document = klass if klass
14
+ @_lycra_document || ("#{name}Document".constantize rescue nil)
15
+ end
16
+
17
+ def document=(klass)
18
+ document klass
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ delegate :__lycra__, :as_indexed_json, :indexed, :indexed?, :index!, to: :document
24
+
25
+ def reload
26
+ @document = nil
27
+ super
28
+ end
29
+
30
+ def document(document_class=nil)
31
+ return document_class.new(self) if document_class
32
+ @document ||= self.class.document.new(self)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,485 @@
1
+ require 'lycra/document/registry'
2
+
3
+ module Lycra
4
+ module Document
5
+ module Proxy
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+
10
+ base.class_eval do
11
+ def self.__lycra__(&block)
12
+ @__lycra__ ||= ClassProxy.new(self)
13
+ @__lycra__.instance_eval(&block) if block_given?
14
+ @__lycra__
15
+ end
16
+
17
+ def __lycra__(&block)
18
+ @__lycra__ ||= InstanceProxy.new(self)
19
+ @__lycra__.instance_eval(&block) if block_given?
20
+ @__lycra__
21
+ end
22
+
23
+ self.__lycra__.class_eval do
24
+ include ::Elasticsearch::Model::Importing::ClassMethods
25
+ include ::Elasticsearch::Model::Adapter.from_class(base).importing_mixin
26
+ end
27
+
28
+ Registry.add(base)
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ delegate :alias_name, :index_name, :document_type, :search,
34
+ :alias_exists?, :index_exists?, :index_aliased?, :aliased_index,
35
+ :index_fingerprint, to: :__lycra__
36
+
37
+ def inherited(child)
38
+ super if defined?(super)
39
+
40
+ # resets the proxy so it gets recreated for the new class
41
+ child.send :instance_variable_set, :@__lycra__, nil
42
+ child.send :instance_variable_set, :@_lycra_import_scope, self.import_scope
43
+
44
+ child.class_eval do
45
+ self.__lycra__.class_eval do
46
+ include ::Elasticsearch::Model::Importing::ClassMethods
47
+ include ::Elasticsearch::Model::Adapter.from_class(child).importing_mixin
48
+ end
49
+ end
50
+
51
+ Registry.add(child)
52
+ end
53
+
54
+ def import_scope(scope=nil, &block)
55
+ @_lycra_import_scope = scope if scope
56
+ @_lycra_import_scope = block if block_given?
57
+ @_lycra_import_scope
58
+ end
59
+
60
+ def import_scope=(scope)
61
+ import_scope scope
62
+ end
63
+
64
+ def create_alias!(options={})
65
+ raise Lycra::AbstractClassError, "Cannot create aliases using an abstract class" if abstract?
66
+ __lycra__.create_alias!(options)
67
+ end
68
+
69
+ def create_alias(options={})
70
+ create_alias!(options)
71
+ rescue => e
72
+ Lycra.configuration.logger.error(e.message)
73
+ return false
74
+ end
75
+
76
+ def create_index!(options={})
77
+ raise Lycra::AbstractClassError, "Cannot create indices using an abstract class" if abstract?
78
+ __lycra__.create_index!(options)
79
+ __lycra__.create_alias!(options) unless alias_exists?
80
+ end
81
+
82
+ def create_index(options={})
83
+ create_index!(options)
84
+ rescue => e
85
+ Lycra.configuration.logger.error(e.message)
86
+ return false
87
+ end
88
+
89
+ def delete_alias!(options={})
90
+ raise Lycra::AbstractClassError, "Cannot delete aliases using an abstract class" if abstract?
91
+ __lycra__.delete_alias!(options)
92
+ end
93
+
94
+ def delete_alias(options={})
95
+ delete_alias!(options)
96
+ rescue => e
97
+ Lycra.configuration.logger.error(e.message)
98
+ return false
99
+ end
100
+
101
+ def delete_index!(options={})
102
+ raise Lycra::AbstractClassError, "Cannot delete indices using an abstract class" if abstract?
103
+ __lycra__.delete_alias!(options) if alias_exists?
104
+ __lycra__.delete_index!(options)
105
+ end
106
+
107
+ def delete_index(options={})
108
+ delete_index!(options)
109
+ rescue => e
110
+ Lycra.configuration.logger.error(e.message)
111
+ return false
112
+ end
113
+
114
+ def refresh_index!(options={})
115
+ raise Lycra::AbstractClassError, "Cannot refresh indices using an abstract class" if abstract?
116
+ __lycra__.refresh_index!(options)
117
+ end
118
+
119
+ def refresh_index(options={})
120
+ refresh_index!(options)
121
+ rescue => e
122
+ Lycra.configuration.logger.error(e.message)
123
+ return false
124
+ end
125
+
126
+ def import!(options={}, &block)
127
+ raise Lycra::AbstractClassError, "Cannot import using an abstract class" if abstract?
128
+
129
+ options[:scope] ||= import_scope if import_scope.is_a?(String) || import_scope.is_a?(Symbol)
130
+ options[:query] ||= import_scope if import_scope.is_a?(Proc)
131
+
132
+ __lycra__.import(options, &block)
133
+ end
134
+
135
+ def import(options={}, &block)
136
+ import!(options, &block)
137
+ rescue => e
138
+ Lycra.configuration.logger.error(e.message)
139
+ return false
140
+ end
141
+
142
+ def update!(options={}, &block)
143
+ raise Lycra::AbstractClassError, "Cannot update using an abstract class" if abstract?
144
+
145
+ scope = options[:scope] || options[:query] || import_scope
146
+ if scope.is_a?(Proc)
147
+ scope = subject_type.instance_exec(&scope)
148
+ elsif scope.is_a?(String) || scope.is_a?(Symbol)
149
+ scope = subject_type.send(scope)
150
+ elsif scope.nil?
151
+ scope = subject_type.all
152
+ end
153
+
154
+ scope.find_in_batches(batch_size: (options[:batch_size] || 200)).each do |batch|
155
+ json_options = options.select { |k,v| [:only,:except].include?(k) }
156
+ items = batch.map do |record|
157
+ { update: {
158
+ _index: index_name,
159
+ _type: document_type,
160
+ _id: record.id,
161
+ data: {
162
+ doc: new(record).resolve!(json_options)
163
+ }.stringify_keys
164
+ }.stringify_keys
165
+ }.stringify_keys
166
+ end
167
+
168
+ updated = __lycra__.client.bulk(body: items)
169
+
170
+ missing = updated['items'].map do |miss|
171
+ if miss['update'].key?('error') &&
172
+ miss['update']['error']['type'] == 'document_missing_exception'
173
+
174
+ update = miss['update']
175
+ item = items.find { |i| i['update']['_id'].to_s == miss['update']['_id'] }['update']
176
+ if json_options.empty?
177
+ data = item['data']['doc']
178
+ else
179
+ data = new(subject_type.find(update['_id'])).resolve!
180
+ end
181
+
182
+ { index: {
183
+ _index: update['_index'],
184
+ _type: update['_type'],
185
+ _id: update['_id'],
186
+ data: data
187
+ }.stringify_keys
188
+ }.stringify_keys
189
+ else
190
+ nil
191
+ end
192
+ end.compact
193
+
194
+ if missing.count > 0
195
+ indexed = __lycra__.client.bulk body: missing
196
+
197
+ updated['items'] = updated['items'].map do |item|
198
+ miss = indexed['items'].find { |i| i['index']['_id'] == item['update']['_id'] }
199
+ miss || item
200
+ end
201
+ end
202
+
203
+ yield(updated) if block_given?
204
+ end
205
+
206
+ return true
207
+ end
208
+
209
+ def update(options={}, &block)
210
+ update!(options, &block)
211
+ rescue => e
212
+ Lycra.configuration.logger.error(e.message)
213
+ return false
214
+ end
215
+
216
+ def delete!(options={}, &block)
217
+ raise Lycra::AbstractClassError, "Cannot delete using an abstract class" if abstract?
218
+
219
+ scope = options[:scope] || options[:query] || import_scope
220
+ if scope.is_a?(Proc)
221
+ scope = subject_type.instance_exec(&scope)
222
+ elsif scope.is_a?(String) || scope.is_a?(Symbol)
223
+ scope = subject_type.send(scope)
224
+ elsif scope.nil?
225
+ scope = subject_type.all
226
+ end
227
+
228
+ scope.find_in_batches(batch_size: (options[:batch_size] || 200)).each do |batch|
229
+ items = batch.map do |record|
230
+ { delete: {
231
+ _index: index_name,
232
+ _type: document_type,
233
+ _id: record.id
234
+ }.stringify_keys
235
+ }.stringify_keys
236
+ end
237
+
238
+ deleted = __lycra__.client.bulk(body: items)
239
+
240
+ yield(deleted) if block_given?
241
+ end
242
+
243
+ return true
244
+ end
245
+
246
+ def delete(options={}, &block)
247
+ delete!(options, &block)
248
+ rescue => e
249
+ Lycra.configuration.logger.error(e.message)
250
+ return false
251
+ end
252
+
253
+ def as_indexed_json(subj, options={})
254
+ resolve!(subj).as_json(options)
255
+ end
256
+
257
+ def as_json(options={})
258
+ { index: index_name,
259
+ document: document_type,
260
+ subject: subject_type.name }
261
+ .merge(attributes.map { |k,a| [a.name, a.type.type] }.to_h)
262
+ .as_json(options)
263
+ end
264
+
265
+ def inspect
266
+ "#{name}(index: #{index_name}, document: #{document_type}, subject: #{subject_type}, #{attributes.map { |key,attr| "#{attr.name}: #{attr.nested? ? "[#{attr.type.type}]" : attr.type.type}"}.join(', ')})"
267
+ end
268
+ end
269
+
270
+ module InstanceMethods
271
+ delegate :index_name, :document_type, to: :class
272
+
273
+ def as_indexed_json(options={})
274
+ resolve!.as_json(options)
275
+ end
276
+
277
+ def index!(options={})
278
+ raise Lycra::AbstractClassError, "Cannot index using an abstract class" if abstract?
279
+
280
+ @indexed = nil
281
+ __lycra__.index_document(options)
282
+ end
283
+
284
+ def update!(options={})
285
+ raise Lycra::AbstractClassError, "Cannot update using an abstract class" if abstract?
286
+
287
+ @indexed = nil
288
+ __lycra__.update_document(options)
289
+ end
290
+
291
+ def update_attributes!(*attrs, **options)
292
+ raise Lycra::AbstractClassError, "Cannot update using an abstract class" if abstract?
293
+
294
+ if attrs.empty?
295
+ document_attrs = resolve!
296
+ else
297
+ document_attrs = resolve!(only: attrs)
298
+ end
299
+
300
+ @indexed = nil
301
+ __lycra__.update_document_attributes(document_attrs, options)
302
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
303
+ index!(options)
304
+ end
305
+
306
+ def _indexed
307
+ @indexed ||= self.class.search({query: {terms: {_id: [subject.id]}}}).results.first
308
+ end
309
+
310
+ def indexed
311
+ _indexed&._source&.to_h
312
+ end
313
+
314
+ def indexed?
315
+ !!indexed
316
+ end
317
+
318
+ def _indexed?
319
+ !!@indexed
320
+ end
321
+
322
+ def reload
323
+ super if defined?(super)
324
+ @indexed = nil
325
+ self
326
+ end
327
+
328
+ def as_json(options={})
329
+ resolve! unless resolved?
330
+
331
+ { index: self.class.index_name,
332
+ document: self.class.document_type,
333
+ subject: self.class.subject_type.name,
334
+ resolved: resolved.map { |k,a| [k, a.as_json] }.to_h,
335
+ indexed: indexed? && indexed.map { |k,a| [k, a.as_json] }.to_h }
336
+ .as_json(options)
337
+ end
338
+
339
+ def inspect
340
+ attr_str = "#{attributes.map { |key,attr| "#{key}: #{(resolved? && resolved[key].try(:to_json)) || (_indexed? && indexed[key.to_s].try(:to_json)) || (attr.nested? ? "[#{attr.type.type}]" : attr.type.type)}"}.join(', ')}>"
341
+ "#<#{self.class.name} index: #{self.class.index_name}, document: #{self.class.document_type}, subject: #{self.class.subject_type}, #{attr_str}"
342
+ end
343
+ end
344
+
345
+ module BaseProxy
346
+ attr_reader :target
347
+
348
+ def initialize(target)
349
+ @target = target
350
+ end
351
+
352
+ def client=(client)
353
+ @client = client
354
+ end
355
+
356
+ def client
357
+ @client ||= Lycra.client
358
+ end
359
+
360
+ def method_missing(meth, *args, &block)
361
+ return target.send(meth, *args, &block) if target.respond_to?(meth)
362
+ super
363
+ end
364
+
365
+ def respond_to_missing?(meth, priv=false)
366
+ target.respond_to?(meth, priv) || super
367
+ end
368
+ end
369
+
370
+ class ClassProxy
371
+ include BaseProxy
372
+ delegate :subject_type, :import_scope, to: :target
373
+
374
+ # this is copying their (annoying) pattern
375
+ class_eval do
376
+ include ::Elasticsearch::Model::Indexing::ClassMethods
377
+ include ::Elasticsearch::Model::Searching::ClassMethods
378
+ end
379
+
380
+ def index_fingerprint(hashed=nil)
381
+ @_lycra_index_fingerprint = hashed if hashed
382
+ @_lycra_index_fingerprint ||= Digest::MD5.hexdigest(mappings.to_s)
383
+
384
+ if @_lycra_index_fingerprint.is_a?(Proc)
385
+ instance_exec(&@_lycra_index_fingerprint)
386
+ else
387
+ @_lycra_index_fingerprint
388
+ end
389
+ end
390
+
391
+ def index_fingerprint=(hashed)
392
+ index_fingerprint hashed
393
+ end
394
+
395
+ def alias_name(index_alias=nil)
396
+ @_lycra_alias_name = index_alias if index_alias
397
+ @_lycra_alias_name ||= document_type.pluralize
398
+ end
399
+
400
+ def alias_name=(index_alias)
401
+ alias_name index_alias
402
+ end
403
+
404
+ def index_name(index=nil)
405
+ @_lycra_index_name = index if index
406
+ @_lycra_index_name ||= "#{alias_name}-#{index_fingerprint}"
407
+ end
408
+
409
+ def index_name=(index)
410
+ index_name index
411
+ end
412
+
413
+ def document_type(type=nil)
414
+ @_lycra_document_type = type if type
415
+ @_lycra_document_type ||= target.name.demodulize.gsub(/Document\Z/, '').underscore
416
+ end
417
+
418
+ def document_type=(type)
419
+ document_type type
420
+ end
421
+
422
+ def mapping(mapping=nil)
423
+ @_lycra_mapping = mapping if mapping
424
+ { document_type.to_s.underscore.to_sym => (@_lycra_mapping || {}).merge({
425
+ properties: attributes.map { |name, type| [name, type.mapping] }.to_h
426
+ }) }
427
+ end
428
+ alias_method :mappings, :mapping
429
+
430
+ def settings(settings=nil)
431
+ @_lycra_settings = settings if settings
432
+ @_lycra_settings || {}
433
+ end
434
+
435
+ def search(query_or_payload, options={})
436
+ options = {index: alias_name}.merge(options)
437
+ super(query_or_payload, options)
438
+ end
439
+
440
+ def alias_exists?
441
+ client.indices.exists_alias? name: alias_name
442
+ end
443
+
444
+ def aliased_index
445
+ client.indices.get_alias(name: alias_name).keys.first
446
+ end
447
+
448
+ def index_aliased?
449
+ alias_exists? && aliased_index == index_name
450
+ end
451
+
452
+ def create_alias!(options={})
453
+ # TODO custom error classes
454
+ raise "Alias already exists" if alias_exists?
455
+ client.indices.put_alias name: alias_name, index: index_name
456
+ end
457
+
458
+ def delete_alias!(options={})
459
+ # TODO custom error classes
460
+ raise "Alias does not exists" unless alias_exists?
461
+ client.indices.delete_alias name: alias_name, index: aliased_index
462
+ end
463
+ end
464
+
465
+ class InstanceProxy
466
+ include BaseProxy
467
+ delegate :index_name, :document_type, to: :klass_proxy
468
+ delegate :subject_type, to: :klass
469
+
470
+ # this is copying their (annoying) pattern
471
+ class_eval do
472
+ include ::Elasticsearch::Model::Indexing::InstanceMethods
473
+ end
474
+
475
+ def klass
476
+ target.class
477
+ end
478
+
479
+ def klass_proxy
480
+ klass.__lycra__
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end