duck_record 0.0.20 → 0.0.21

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.
@@ -0,0 +1,11 @@
1
+ module DuckRecord::Associations
2
+ module ForeignAssociation # :nodoc:
3
+ def foreign_key_present?
4
+ if reflection.klass.primary_key
5
+ owner.attribute_present?(reflection.active_record_primary_key)
6
+ else
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module DuckRecord
2
+ # = Active Record Has Many Association
3
+ module Associations
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+ include ForeignAssociation
10
+
11
+ def insert_record(record, validate = true, raise = false)
12
+ set_owner_attributes(record)
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ module DuckRecord
2
+ # = Active Record Has One Association
3
+ module Associations
4
+ class HasOneAssociation < SingularAssociation #:nodoc:
5
+ include ForeignAssociation
6
+
7
+ def replace(record)
8
+ if owner.class.readonly_attributes.include?(reflection.foreign_key.to_s)
9
+ return
10
+ end
11
+
12
+ raise_on_type_mismatch!(record) if record
13
+ load_target
14
+
15
+ return target unless target || record
16
+
17
+ self.target = record
18
+ end
19
+
20
+ private
21
+
22
+ def foreign_key_present?
23
+ true
24
+ end
25
+
26
+ # The reason that the save param for replace is false, if for create (not just build),
27
+ # is because the setting of the foreign keys is actually handled by the scoping when
28
+ # the record is instantiated, and so they are set straight away and do not need to be
29
+ # updated within replace.
30
+ def set_new_record(record)
31
+ replace(record)
32
+ end
33
+
34
+ def nullify_owner_attributes(record)
35
+ record[reflection.foreign_key] = nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ module DuckRecord
2
+ module Associations
3
+ class SingularAssociation < Association #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader
6
+ if !loaded? || stale_target?
7
+ reload
8
+ end
9
+
10
+ target
11
+ end
12
+
13
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
14
+ def writer(record)
15
+ replace(record)
16
+ end
17
+
18
+ def build(attributes = {})
19
+ record = build_record(attributes)
20
+ yield(record) if block_given?
21
+ set_new_record(record)
22
+ record
23
+ end
24
+
25
+ # Implements the reload reader method, e.g. foo.reload_bar for
26
+ # Foo.has_one :bar
27
+ def force_reload_reader
28
+ klass.uncached { reload }
29
+ target
30
+ end
31
+
32
+ private
33
+
34
+ def create_scope
35
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
36
+ end
37
+
38
+ def find_target
39
+ return scope.take if skip_statement_cache?
40
+
41
+ conn = klass.connection
42
+ sc = reflection.association_scope_cache(conn, owner) do
43
+ ActiveRecord::StatementCache.create(conn) { |params|
44
+ as = ActiveRecord::Associations::AssociationScope.create { params.bind }
45
+ target_scope.merge(as.scope(self, conn)).limit(1)
46
+ }
47
+ end
48
+
49
+ binds = ActiveRecord::Associations::AssociationScope.get_bind_values(owner, reflection.chain)
50
+ sc.execute(binds, klass, conn).first
51
+ rescue ::RangeError
52
+ nil
53
+ end
54
+
55
+ def replace(record)
56
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
57
+ end
58
+
59
+ def set_new_record(record)
60
+ replace(record)
61
+ end
62
+
63
+ def _create_record(attributes, raise_error = false)
64
+ record = build_record(attributes)
65
+ yield(record) if block_given?
66
+ saved = record.save
67
+ set_new_record(record)
68
+ raise ActiveRecord::RecordInvalid.new(record) if !saved && raise_error
69
+ record
70
+ end
71
+ end
72
+ end
73
+ end
@@ -139,7 +139,7 @@ module DuckRecord
139
139
  end
140
140
 
141
141
  included do
142
- Associations::Builder::EmbedsAssociation.extensions << AssociationBuilderExtension
142
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
143
143
  mattr_accessor :index_nested_attribute_errors, instance_writer: false
144
144
  self.index_nested_attribute_errors = false
145
145
  end
@@ -12,18 +12,24 @@ module DuckRecord
12
12
  self._reflections = {}
13
13
  end
14
14
 
15
- def self.create(macro, name, options, ar)
15
+ def self.create(macro, name, scope, options, ar)
16
16
  klass = \
17
17
  case macro
18
18
  when :embeds_many
19
19
  EmbedsManyReflection
20
20
  when :embeds_one
21
21
  EmbedsOneReflection
22
+ when :belongs_to
23
+ BelongsToReflection
24
+ when :has_many
25
+ HasManyReflection
26
+ when :has_one
27
+ HasOneReflection
22
28
  else
23
29
  raise "Unsupported Macro: #{macro}"
24
30
  end
25
31
 
26
- klass.new(name, options, ar)
32
+ klass.new(name, scope, options, ar)
27
33
  end
28
34
 
29
35
  def self.add_reflection(ar, name, reflection)
@@ -110,7 +116,7 @@ module DuckRecord
110
116
  # ThroughReflection
111
117
  # PolymorphicReflection
112
118
  # RuntimeReflection
113
- class AbstractReflection # :nodoc:
119
+ class AbstractReflection
114
120
  # Returns a new, unsaved instance of the associated class. +attributes+ will
115
121
  # be passed to the class's constructor.
116
122
  def build_association(attributes, &block)
@@ -143,6 +149,8 @@ module DuckRecord
143
149
  # <tt>has_many :clients</tt> returns <tt>:clients</tt>
144
150
  attr_reader :name
145
151
 
152
+ attr_reader :scope
153
+
146
154
  # Returns the hash of options used for the macro.
147
155
  #
148
156
  # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
@@ -153,8 +161,9 @@ module DuckRecord
153
161
 
154
162
  attr_reader :plural_name # :nodoc:
155
163
 
156
- def initialize(name, options, duck_record)
164
+ def initialize(name, scope, options, duck_record)
157
165
  @name = name
166
+ @scope = scope
158
167
  @options = options
159
168
  @duck_record = duck_record
160
169
  @klass = options[:anonymous_class]
@@ -173,6 +182,16 @@ module DuckRecord
173
182
  name.constantize
174
183
  end
175
184
 
185
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
186
+ # and +other_aggregation+ has an options hash assigned to it.
187
+ def ==(other_aggregation)
188
+ super ||
189
+ other_aggregation.kind_of?(self.class) &&
190
+ name == other_aggregation.name &&
191
+ !other_aggregation.options.nil? &&
192
+ active_record == other_aggregation.active_record
193
+ end
194
+
176
195
  private
177
196
 
178
197
  def derive_class_name
@@ -182,7 +201,7 @@ module DuckRecord
182
201
 
183
202
  # Holds all the metadata about an association as it was specified in the
184
203
  # Active Record class.
185
- class EmbedsAssociationReflection < MacroReflection #:nodoc:
204
+ class EmbedsAssociationReflection < MacroReflection
186
205
  # Returns the target association's class.
187
206
  #
188
207
  # class Author < ActiveRecord::Base
@@ -205,7 +224,7 @@ module DuckRecord
205
224
 
206
225
  attr_accessor :parent_reflection # Reflection
207
226
 
208
- def initialize(name, options, duck_record)
227
+ def initialize(name, scope, options, duck_record)
209
228
  super
210
229
  @constructable = calculate_constructable(macro, options)
211
230
 
@@ -286,7 +305,7 @@ module DuckRecord
286
305
  end
287
306
  end
288
307
 
289
- class EmbedsManyReflection < EmbedsAssociationReflection # :nodoc:
308
+ class EmbedsManyReflection < EmbedsAssociationReflection
290
309
  def macro; :embeds_many; end
291
310
 
292
311
  def collection?; true; end
@@ -296,7 +315,7 @@ module DuckRecord
296
315
  end
297
316
  end
298
317
 
299
- class EmbedsOneReflection < EmbedsAssociationReflection # :nodoc:
318
+ class EmbedsOneReflection < EmbedsAssociationReflection
300
319
  def macro; :embeds_one; end
301
320
 
302
321
  def has_one?; true; end
@@ -305,5 +324,323 @@ module DuckRecord
305
324
  Associations::EmbedsOneAssociation
306
325
  end
307
326
  end
327
+
328
+ # Holds all the metadata about an association as it was specified in the
329
+ # Active Record class.
330
+ class AssociationReflection < MacroReflection
331
+ alias active_record duck_record
332
+
333
+ def quoted_table_name
334
+ klass.quoted_table_name
335
+ end
336
+
337
+ def primary_key_type
338
+ klass.type_for_attribute(klass.primary_key)
339
+ end
340
+
341
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
342
+
343
+ def join_keys
344
+ get_join_keys klass
345
+ end
346
+
347
+ # Returns a list of scopes that should be applied for this Reflection
348
+ # object when querying the database.
349
+ def scopes
350
+ scope ? [scope] : []
351
+ end
352
+
353
+ def scope_chain
354
+ chain.map(&:scopes)
355
+ end
356
+ deprecate :scope_chain
357
+
358
+ def join_scopes(table, predicate_builder) # :nodoc:
359
+ if scope
360
+ [ActiveRecord::Relation.create(klass, table, predicate_builder)
361
+ .instance_exec(&scope)]
362
+ else
363
+ []
364
+ end
365
+ end
366
+
367
+ def klass_join_scope(table, predicate_builder) # :nodoc:
368
+ relation = ActiveRecord::Relation.create(klass, table, predicate_builder)
369
+ klass.scope_for_association(relation)
370
+ end
371
+
372
+ def constraints
373
+ chain.map(&:scopes).flatten
374
+ end
375
+
376
+ def alias_candidate(name)
377
+ "#{plural_name}_#{name}"
378
+ end
379
+
380
+ def chain
381
+ collect_join_chain
382
+ end
383
+
384
+ def get_join_keys(association_klass)
385
+ JoinKeys.new(join_pk(association_klass), join_fk)
386
+ end
387
+
388
+ # Returns the target association's class.
389
+ #
390
+ # class Author < ActiveRecord::Base
391
+ # has_many :books
392
+ # end
393
+ #
394
+ # Author.reflect_on_association(:books).klass
395
+ # # => Book
396
+ #
397
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
398
+ # a new association object. Use +build_association+ or +create_association+
399
+ # instead. This allows plugins to hook into association object creation.
400
+ def klass
401
+ @klass ||= compute_class(class_name)
402
+ end
403
+
404
+ def compute_class(name)
405
+ active_record.send(:compute_type, name)
406
+ end
407
+
408
+ def table_name
409
+ klass.table_name
410
+ end
411
+
412
+ attr_reader :type, :foreign_type
413
+ attr_accessor :parent_reflection # Reflection
414
+
415
+ def initialize(name, scope, options, active_record)
416
+ super
417
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
418
+ @foreign_type = options[:foreign_type] || "#{name}_type"
419
+ @constructable = calculate_constructable(macro, options)
420
+ @association_scope_cache = {}
421
+ @scope_lock = Mutex.new
422
+
423
+ if options[:class_name] && options[:class_name].class == Class
424
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
425
+ Passing a class to the `class_name` is deprecated and will raise
426
+ an ArgumentError in Rails 5.2. It eagerloads more classes than
427
+ necessary and potentially creates circular dependencies.
428
+
429
+ Please pass the class name as a string:
430
+ `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
431
+ MSG
432
+ end
433
+ end
434
+
435
+ def association_scope_cache(conn, owner)
436
+ key = conn.prepared_statements
437
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
438
+ @association_scope_cache[key] ||= yield
439
+ }
440
+ end
441
+
442
+ def constructable? # :nodoc:
443
+ @constructable
444
+ end
445
+
446
+ def join_table
447
+ @join_table ||= options[:join_table] || derive_join_table
448
+ end
449
+
450
+ def foreign_key
451
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
452
+ end
453
+
454
+ def association_foreign_key
455
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
456
+ end
457
+
458
+ # klass option is necessary to support loading polymorphic associations
459
+ def association_primary_key(klass = nil)
460
+ options[:primary_key] || primary_key(klass || self.klass)
461
+ end
462
+
463
+ def association_primary_key_type
464
+ klass.type_for_attribute(association_primary_key.to_s)
465
+ end
466
+
467
+ def active_record_primary_key
468
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
469
+ end
470
+
471
+ def check_validity!
472
+ unless klass < ActiveRecord::Base
473
+ raise ArgumentError, "#{klass} must be inherited from ActiveRecord::Base."
474
+ end
475
+ end
476
+
477
+ def check_preloadable!
478
+ return unless scope
479
+
480
+ if scope.arity > 0
481
+ raise ArgumentError, <<-MSG.squish
482
+ The association scope '#{name}' is instance dependent (the scope
483
+ block takes an argument). Preloading instance dependent scopes is
484
+ not supported.
485
+ MSG
486
+ end
487
+ end
488
+ alias :check_eager_loadable! :check_preloadable!
489
+
490
+ def join_id_for(owner) # :nodoc:
491
+ owner[active_record_primary_key]
492
+ end
493
+
494
+ def source_reflection
495
+ self
496
+ end
497
+
498
+ # A chain of reflections from this one back to the owner. For more see the explanation in
499
+ # ThroughReflection.
500
+ def collect_join_chain
501
+ [self]
502
+ end
503
+
504
+ # This is for clearing cache on the reflection. Useful for tests that need to compare
505
+ # SQL queries on associations.
506
+ def clear_association_scope_cache # :nodoc:
507
+ @association_scope_cache.clear
508
+ end
509
+
510
+ def nested?
511
+ false
512
+ end
513
+
514
+ def has_scope?
515
+ scope
516
+ end
517
+
518
+ # Returns the macro type.
519
+ #
520
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
521
+ def macro; raise NotImplementedError; end
522
+
523
+ # Returns whether or not this association reflection is for a collection
524
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
525
+ # +has_and_belongs_to_many+, +false+ otherwise.
526
+ def collection?
527
+ false
528
+ end
529
+
530
+ # Returns whether or not the association should be validated as part of
531
+ # the parent's validation.
532
+ #
533
+ # Unless you explicitly disable validation with
534
+ # <tt>validate: false</tt>, validation will take place when:
535
+ #
536
+ # * you explicitly enable validation; <tt>validate: true</tt>
537
+ # * you use autosave; <tt>autosave: true</tt>
538
+ # * the association is a +has_many+ association
539
+ def validate?
540
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
541
+ end
542
+
543
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
544
+ def belongs_to?; false; end
545
+
546
+ # Returns +true+ if +self+ is a +has_one+ reflection.
547
+ def has_one?; false; end
548
+
549
+ def association_class; raise NotImplementedError; end
550
+
551
+ def add_as_source(seed)
552
+ seed
553
+ end
554
+
555
+ def extensions
556
+ Array(options[:extend])
557
+ end
558
+
559
+ protected
560
+
561
+ def actual_source_reflection # FIXME: this is a horrible name
562
+ self
563
+ end
564
+
565
+ private
566
+
567
+ def join_pk(_)
568
+ foreign_key
569
+ end
570
+
571
+ def join_fk
572
+ active_record_primary_key
573
+ end
574
+
575
+ def calculate_constructable(_macro, _options)
576
+ false
577
+ end
578
+
579
+ def derive_class_name
580
+ class_name = name.to_s
581
+ class_name = class_name.singularize if collection?
582
+ class_name.camelize
583
+ end
584
+
585
+ def derive_foreign_key
586
+ if options[:as]
587
+ "#{options[:as]}_id"
588
+ else
589
+ "#{name}_id"
590
+ end
591
+ end
592
+
593
+ def primary_key(klass)
594
+ klass.primary_key || raise(ActiveRecord::UnknownPrimaryKey.new(klass))
595
+ end
596
+ end
597
+
598
+ class BelongsToReflection < AssociationReflection # :nodoc:
599
+ def macro; :belongs_to; end
600
+
601
+ def belongs_to?; true; end
602
+
603
+ def association_class
604
+ Associations::BelongsToAssociation
605
+ end
606
+
607
+ def join_id_for(owner) # :nodoc:
608
+ owner[foreign_key]
609
+ end
610
+
611
+ private
612
+
613
+ def join_fk
614
+ foreign_key
615
+ end
616
+
617
+ def join_pk(_klass)
618
+ association_primary_key
619
+ end
620
+ end
621
+
622
+ class HasManyReflection < AssociationReflection # :nodoc:
623
+ def macro; :has_many; end
624
+
625
+ def collection?; true; end
626
+
627
+ def association_class
628
+ Associations::HasManyAssociation
629
+ end
630
+
631
+ def association_primary_key(klass = nil)
632
+ primary_key(klass || self.klass)
633
+ end
634
+ end
635
+
636
+ class HasOneReflection < AssociationReflection # :nodoc:
637
+ def macro; :has_one; end
638
+
639
+ def has_one?; true; end
640
+
641
+ def association_class
642
+ Associations::HasOneAssociation
643
+ end
644
+ end
308
645
  end
309
646
  end