all_futures 1.0.2 → 2.0.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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +297 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +59 -77
  6. data/Rakefile +13 -1
  7. data/SUMMARY.md +22 -2
  8. data/bin/console +8 -0
  9. data/bin/loc +0 -0
  10. data/bin/setup +0 -0
  11. data/bin/standardize +0 -0
  12. data/lib/all_futures/association.rb +42 -0
  13. data/lib/all_futures/association_index.rb +69 -0
  14. data/lib/all_futures/attributes.rb +28 -0
  15. data/lib/all_futures/base.rb +66 -0
  16. data/lib/all_futures/callbacks.rb +71 -0
  17. data/lib/all_futures/configuration.rb +9 -0
  18. data/lib/all_futures/dirty.rb +116 -0
  19. data/lib/all_futures/embeds.rb +40 -0
  20. data/lib/all_futures/errors.rb +37 -0
  21. data/lib/all_futures/finder.rb +214 -0
  22. data/lib/all_futures/locale/en.yml +49 -0
  23. data/lib/all_futures/persistence.rb +595 -0
  24. data/lib/all_futures/presenter.rb +35 -0
  25. data/lib/all_futures/railtie.rb +26 -0
  26. data/lib/all_futures/translation.rb +22 -0
  27. data/lib/all_futures/validations.rb +10 -0
  28. data/lib/all_futures/version.rb +3 -1
  29. data/lib/all_futures/versions.rb +80 -0
  30. data/lib/all_futures.rb +24 -1
  31. data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
  32. data/lib/vendor/README.md +8 -0
  33. data/lib/vendor/active_entity/aggregations.rb +277 -0
  34. data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
  35. data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
  36. data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
  37. data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
  38. data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
  39. data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
  40. data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
  41. data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
  42. data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
  43. data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
  44. data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
  45. data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
  46. data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
  47. data/lib/vendor/active_entity/associations.rb +168 -0
  48. data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
  49. data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
  50. data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
  51. data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
  52. data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
  53. data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
  54. data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
  55. data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
  56. data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
  57. data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
  58. data/lib/vendor/active_entity/attribute_methods.rb +833 -0
  59. data/lib/vendor/active_entity/attributes.rb +240 -0
  60. data/lib/vendor/active_entity/base.rb +293 -0
  61. data/lib/vendor/active_entity/callbacks.rb +21 -0
  62. data/lib/vendor/active_entity/coders/json.rb +15 -0
  63. data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
  64. data/lib/vendor/active_entity/core.rb +348 -0
  65. data/lib/vendor/active_entity/enum.rb +260 -0
  66. data/lib/vendor/active_entity/errors.rb +69 -0
  67. data/lib/vendor/active_entity/gem_version.rb +17 -0
  68. data/lib/vendor/active_entity/inheritance.rb +176 -0
  69. data/lib/vendor/active_entity/integration.rb +78 -0
  70. data/lib/vendor/active_entity/locale/en.yml +45 -0
  71. data/lib/vendor/active_entity/model_schema.rb +118 -0
  72. data/lib/vendor/active_entity/nested_attributes.rb +554 -0
  73. data/lib/vendor/active_entity/persistence.rb +20 -0
  74. data/lib/vendor/active_entity/railtie.rb +76 -0
  75. data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
  76. data/lib/vendor/active_entity/reflection.rb +447 -0
  77. data/lib/vendor/active_entity/serialization.rb +28 -0
  78. data/lib/vendor/active_entity/store.rb +291 -0
  79. data/lib/vendor/active_entity/translation.rb +24 -0
  80. data/lib/vendor/active_entity/type/date.rb +9 -0
  81. data/lib/vendor/active_entity/type/date_time.rb +9 -0
  82. data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
  83. data/lib/vendor/active_entity/type/json.rb +30 -0
  84. data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
  85. data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
  86. data/lib/vendor/active_entity/type/registry.rb +112 -0
  87. data/lib/vendor/active_entity/type/serialized.rb +71 -0
  88. data/lib/vendor/active_entity/type/text.rb +11 -0
  89. data/lib/vendor/active_entity/type/time.rb +21 -0
  90. data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
  91. data/lib/vendor/active_entity/type.rb +75 -0
  92. data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
  93. data/lib/vendor/active_entity/validations/absence.rb +25 -0
  94. data/lib/vendor/active_entity/validations/associated.rb +60 -0
  95. data/lib/vendor/active_entity/validations/length.rb +26 -0
  96. data/lib/vendor/active_entity/validations/presence.rb +68 -0
  97. data/lib/vendor/active_entity/validations/subset.rb +76 -0
  98. data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
  99. data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
  100. data/lib/vendor/active_entity/validations.rb +47 -0
  101. data/lib/vendor/active_entity/version.rb +10 -0
  102. data/lib/vendor/active_entity.rb +99 -0
  103. data/lib/vendor/core_ext/array_without_blank.rb +48 -0
  104. metadata +172 -31
  105. data/lib/all_futures/all_futures.rb +0 -28
  106. data/lib/all_futures/config.rb +0 -14
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ # Maintains Redis SETs that index embedded children by owner so associations
5
+ # can be lazy-loaded after find without scanning every key of the child class.
6
+ #
7
+ # Key shape: "OwnerClass:owner_id:association_name" => SET of child ids
8
+ module AssociationIndex
9
+ module_function
10
+
11
+ def key(owner_class, owner_id, association_name)
12
+ "#{owner_class.name}:#{owner_id}:#{association_name}"
13
+ end
14
+
15
+ def add(owner_class, owner_id, association_name, child_id)
16
+ return if owner_id.blank? || child_id.blank?
17
+ Kredis.redis.sadd(key(owner_class, owner_id, association_name), child_id.to_s)
18
+ end
19
+
20
+ def remove(owner_class, owner_id, association_name, child_id)
21
+ return if owner_id.blank? || child_id.blank?
22
+ Kredis.redis.srem(key(owner_class, owner_id, association_name), child_id.to_s)
23
+ end
24
+
25
+ def clear(owner_class, owner_id, association_name)
26
+ return if owner_id.blank?
27
+ Kredis.redis.del(key(owner_class, owner_id, association_name))
28
+ end
29
+
30
+ def member_ids(owner_class, owner_id, association_name)
31
+ return [] if owner_id.blank?
32
+ Array(Kredis.redis.smembers(key(owner_class, owner_id, association_name)))
33
+ end
34
+
35
+ def hydrate(association)
36
+ return if association.instance_variable_get(:@af_hydrated)
37
+ association.instance_variable_set(:@af_hydrated, true)
38
+
39
+ owner = association.owner
40
+ return unless owner.is_a?(AllFutures::Base) && owner.persisted?
41
+
42
+ reflection = association.reflection
43
+ return unless [:embeds_many, :embeds_one].include?(reflection.macro)
44
+
45
+ ids = member_ids(owner.class, owner.id, reflection.name)
46
+ return if ids.empty?
47
+
48
+ existing_ids = Array.wrap(association.target).compact.map { |r| r.id.to_s }
49
+
50
+ ids.each do |child_id|
51
+ next if existing_ids.include?(child_id.to_s)
52
+
53
+ begin
54
+ record = reflection.klass.find(child_id)
55
+ rescue AllFutures::RecordNotFound
56
+ remove(owner.class, owner.id, reflection.name, child_id)
57
+ next
58
+ end
59
+
60
+ if reflection.macro == :embeds_many
61
+ association.add_to_target(record, true)
62
+ else
63
+ association.target = record
64
+ association.set_inverse_instance(record)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def has_future(name, klass, **options)
9
+ ivar_symbol = :"@#{name}_all_futures"
10
+
11
+ define_method(name) do
12
+ if instance_variable_defined?(ivar_symbol)
13
+ instance_variable_get(ivar_symbol)
14
+ else
15
+ af_key = if options[:key]
16
+ options[:key]
17
+ else
18
+ record_id = try(:id) or raise AllFutures::ParentModelNotSavedYet, "AllFutures requires a unique key. Either save the parent model before accessing #{name}, or pass a custom key."
19
+ "#{self.class.name.tableize.tr("/", ":")}:#{record_id}:#{name}"
20
+ end
21
+ af = klass.exists?(af_key) ? klass.find(af_key) : klass.new(id: af_key)
22
+ instance_variable_set(ivar_symbol, af)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ class Base < ActiveEntity::Base
5
+ prepend ::AllFutures::Callbacks
6
+ extend ::ActiveModel::Naming
7
+ extend ::AllFutures::Translation
8
+ include ::AllFutures::Persistence
9
+ include ::AllFutures::Dirty
10
+ include ::AllFutures::Validations
11
+ include ::AllFutures::Presenter
12
+ include ::AllFutures::Versions
13
+ include ::AllFutures::Finder
14
+ include ::AllFutures::Embeds
15
+ include ::ActiveModel::Conversion
16
+ include ::ActiveModel::SecurePassword
17
+ include ::ActiveRecord::Integration
18
+ include ::Kredis::Attributes
19
+
20
+ attr_reader :created_at, :updated_at
21
+
22
+ def initialize(attributes = {}, &block)
23
+ attributes ||= {}
24
+ attributes = attributes.attributes.transform_keys(&:to_sym) if attributes.is_a?(ActiveRecord::Base)
25
+ attributes_for_super = attributes.key?(:id) ? attributes.except(:id).except(:created_at).except(:updated_at) : attributes
26
+
27
+ super(attributes_for_super) do
28
+ @id = attributes&.fetch(:id, nil) || ULID.generate
29
+ @created_at = Time.current
30
+ @updated_at = attributes&.fetch(:updated_at, Time.current)
31
+ @redis_key = "#{self.class.name}:#{@id}"
32
+ @new_record = !self.class.exists?(@id)
33
+ @destroyed = false
34
+ @previously_new_record = false
35
+ @_versioning_enabled = self.class.versioning
36
+ @_versions = {}
37
+ @_current_version = nil
38
+ @_id_set = attributes.key?(:id)
39
+
40
+ @attributes.keys.each do |attr|
41
+ define_singleton_method("#{attr}_changed?") { attribute_changed?(attr) }
42
+ define_singleton_method("#{attr}_valid?") { attribute_valid?(attr) }
43
+ define_singleton_method("rollback_#{attr}") { rollback_attribute(attr) }
44
+ define_singleton_method("rollback_#{attr}!") { rollback_attribute!(attr) }
45
+ define_singleton_method("restore_#{attr}") { restore_attribute(attr) }
46
+ define_singleton_method("restore_#{attr}!") { restore_attribute(attr) }
47
+ end
48
+
49
+ block&.call(self)
50
+ end
51
+ end
52
+
53
+ def id
54
+ (!new_record? || @_id_set) ? @id.to_s : nil
55
+ end
56
+
57
+ def id=(value)
58
+ raise FrozenError.new("can't modify id when persisted") unless new_record?
59
+ @id = value.to_s
60
+ @redis_key = "#{self.class.name}:#{@id}"
61
+ @_id_set = true
62
+ end
63
+ end
64
+
65
+ ActiveSupport.run_load_hooks(:all_futures, Base)
66
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module AllFutures
6
+ module Callbacks
7
+ extend ActiveSupport::Concern
8
+
9
+ CALLBACKS = [:before_save, :after_save, :before_update, :after_update, :before_destroy, :after_destroy]
10
+
11
+ def self.prepended(base)
12
+ base.include(ActiveSupport::Callbacks)
13
+
14
+ base.define_callbacks(:save, :update, :destroy, skip_after_callbacks_if_terminated: true) unless base.respond_to?(:_perform_callbacks) && base._perform_callbacks.present?
15
+
16
+ class << base
17
+ prepend ClassMethods
18
+ end
19
+ end
20
+
21
+ def destroy
22
+ perform_operation(:destroy)
23
+ end
24
+
25
+ def destroy!
26
+ perform_operation(:destroy!)
27
+ end
28
+
29
+ def save
30
+ perform_operation(:save)
31
+ end
32
+
33
+ def save!
34
+ perform_operation(:save!)
35
+ end
36
+
37
+ def update(attrs = {})
38
+ perform_operation(:update, attrs)
39
+ end
40
+
41
+ def update!(attrs = {})
42
+ perform_operation(:update!, attrs)
43
+ end
44
+
45
+ def perform_operation(operation, *args)
46
+ if respond_to?(:run_callbacks)
47
+ callback = operation.to_s.sub("!", "").to_sym
48
+ run_callbacks callback do
49
+ method(operation).super_method.call(*args)
50
+ end
51
+ else
52
+ method(operation).super_method.call(*args)
53
+ end
54
+ end
55
+
56
+ module ClassMethods
57
+ def method_missing(name, *filters, &blk)
58
+ if CALLBACKS.include? name
59
+ callback_context, method_name = name.to_s.split("_")
60
+ set_callback(method_name.to_sym, callback_context.to_sym, *filters, &blk)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def respond_to_missing?(name, include_all)
67
+ CALLBACKS.include?(name) || super
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ # When true, versioned models save via a Redis Lua compare-and-set so two
5
+ # processes cannot both pass a stale check and silently clobber each other.
6
+ # Default false: the in-process guard rail (GET, check, SET) is enough for
7
+ # most apps and avoids an EVAL on every write.
8
+ mattr_accessor :atomic_locking, instance_accessor: false, default: false
9
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ module Dirty
5
+ extend ActiveSupport::Concern
6
+
7
+ def attribute_change(attribute)
8
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
9
+ (attribute_was(attribute) == self[attribute]) ? nil : [attribute_was(attribute), self[attribute]]
10
+ end
11
+
12
+ def attribute_changed?(attribute)
13
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
14
+ changes.key? attribute.to_s
15
+ end
16
+ alias_method :attribute_will_change?, :attribute_changed?
17
+ alias_method :will_save_change_to_attribute?, :attribute_changed?
18
+
19
+ def attribute_changed!(attribute)
20
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
21
+ public_send("#{attribute}_will_change!")
22
+ end
23
+
24
+ def attribute_previous_change(attribute)
25
+ super
26
+ end
27
+
28
+ def attribute_will_change!(attribute)
29
+ super
30
+ end
31
+
32
+ def clear_attribute_change(attribute)
33
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
34
+ super
35
+ end
36
+
37
+ def clear_attribute_changes(attr_names = changed)
38
+ super
39
+ end
40
+
41
+ def dirty?
42
+ changes.any?
43
+ end
44
+ alias_method :changed_attributes?, :dirty?
45
+ alias_method :has_changes_to_save?, :dirty?
46
+
47
+ def previous_attributes
48
+ attribute_names.each_with_object({}) do |attribute, hash|
49
+ hash[attribute] = attribute_previously_was(attribute)
50
+ end
51
+ end
52
+
53
+ def mutations_before_last_save
54
+ super
55
+ end
56
+
57
+ def restore_attribute(attribute)
58
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
59
+ attribute = attribute.to_s
60
+ if attribute_changed?(attribute)
61
+ self[attribute] = attribute_was(attribute)
62
+ clear_attribute_change(attribute)
63
+ end
64
+ end
65
+
66
+ def restore_attributes(attr_names = changed)
67
+ attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }.each { |attribute| restore_attribute(attribute) }
68
+ end
69
+
70
+ def rollback_attribute(attribute)
71
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
72
+ if self[attribute] != attribute_previously_was(attribute)
73
+ self[attribute] = attribute_previously_was(attribute)
74
+ clear_attribute_change(attribute)
75
+ end
76
+ end
77
+
78
+ def rollback_attribute!(attribute)
79
+ _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
80
+ # unlike rollback_attribute, keep the mutation tracked so save sees a dirty record
81
+ self[attribute] = attribute_previously_was(attribute) if self[attribute] != attribute_previously_was(attribute)
82
+ save
83
+ end
84
+
85
+ def rollback_attributes(attr_names = changed)
86
+ attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }.each { |attribute| rollback_attribute(attribute) }
87
+ end
88
+
89
+ def rollback_attributes!(attr_names = changed)
90
+ attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }
91
+ attr_names.each { |attribute| self[attribute] = attribute_previously_was(attribute) if self[attribute] != attribute_previously_was(attribute) }
92
+ save
93
+ end
94
+
95
+ def saved_changes?
96
+ saved_changes.any?
97
+ end
98
+ alias_method :previous_changes?, :saved_changes?
99
+
100
+ def saved_changes
101
+ attributes.select { |attribute| attribute_previously_changed? attribute }
102
+ end
103
+
104
+ module ClassMethods
105
+ def set_previous_attributes(model, record)
106
+ tracker = ActiveModel::AttributeMutationTracker.new(model.instance_variable_get(:@attributes))
107
+ previous_values = tracker.instance_variable_get(:@attributes).instance_variable_get(:@attributes)
108
+ previous_values.each do |key, attribute|
109
+ original = attribute.instance_variable_get(:@original_attribute)
110
+ original&.instance_variable_set(:@value_before_type_cast, record["previous_attributes"][key])
111
+ end
112
+ model.instance_variable_set :@mutations_before_last_save, tracker
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ module Embeds
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def embedded_in(name, **options)
9
+ autosave = options.delete(:autosave)
10
+ dependent = options.delete(:dependent)
11
+ foreign_key = options.delete(:foreign_key)
12
+ embeds = super
13
+ embeds[name.to_s].options[:autosave] = autosave
14
+ embeds[name.to_s].options[:dependent] = dependent
15
+ # FK lives on the child and points at the parent: government_id, not spy_id
16
+ embeds[name.to_s].options[:foreign_key] = foreign_key || "#{name}_id"
17
+ end
18
+
19
+ def embeds_one(name, **options)
20
+ autosave = options.delete(:autosave)
21
+ dependent = options.delete(:dependent)
22
+ foreign_key = options.delete(:foreign_key)
23
+ embeds = super
24
+ embeds[name.to_s].options[:autosave] = autosave
25
+ embeds[name.to_s].options[:dependent] = dependent
26
+ embeds[name.to_s].options[:foreign_key] = foreign_key || model_name.singular + "_id"
27
+ end
28
+
29
+ def embeds_many(name, **options)
30
+ autosave = options.delete(:autosave)
31
+ dependent = options.delete(:dependent)
32
+ foreign_key = options.delete(:foreign_key)
33
+ embeds = super
34
+ embeds[name.to_s].options[:autosave] = autosave
35
+ embeds[name.to_s].options[:dependent] = dependent
36
+ embeds[name.to_s].options[:foreign_key] = foreign_key || model_name.singular + "_id"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ class DeleteRestrictionError < StandardError
5
+ def initialize(name = nil)
6
+ if name
7
+ super("Cannot delete record because of dependent #{name}")
8
+ else
9
+ super("Delete restriction error.")
10
+ end
11
+ end
12
+ end
13
+
14
+ class InvalidAttribute < StandardError; end
15
+
16
+ class InvalidDependentOption < StandardError; end
17
+
18
+ class MissingForeignKeyError < StandardError; end
19
+
20
+ class ParentModelNotSavedYet < StandardError; end
21
+
22
+ class ReadOnlyRecord < StandardError; end
23
+
24
+ class RecordInvalid < StandardError; end
25
+
26
+ class RecordNotFound < StandardError; end
27
+
28
+ class RecordNotDestroyed < StandardError; end
29
+
30
+ class RecordNotSaved < StandardError; end
31
+
32
+ class RecordStale < StandardError; end
33
+
34
+ class SoleRecordExceeded < StandardError; end
35
+
36
+ class VersionNotFound < StandardError; end
37
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AllFutures
4
+ module Finder
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def all
9
+ Kredis.redis.scan_each(match: "#{name}:*").map do |key|
10
+ find(key.delete_prefix("#{name}:"))
11
+ end.sort_by(&:created_at)
12
+ end
13
+
14
+ def any?(&block)
15
+ block ? all.any?(&block) : all.any?
16
+ end
17
+
18
+ def count
19
+ Kredis.redis.keys("#{name}:*").size
20
+ end
21
+
22
+ def exists?(id)
23
+ return all.any? unless id
24
+ return false if id == false
25
+ [Hash, Array].include?(id.class) ? where(id).any? : Kredis.redis.exists?("#{name}:#{id}")
26
+ end
27
+
28
+ def fifth
29
+ find_nth 4
30
+ end
31
+
32
+ def fifth!
33
+ fifth || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
34
+ end
35
+
36
+ def find(*ids)
37
+ raise AllFutures::RecordNotFound.new("Couldn't find #{name} without an id") unless ids.flatten.present?
38
+ if ids.size == 1 && [String, Integer, Symbol].include?(ids.first.class)
39
+ record = load_model(ids.first)
40
+ model = new record["attributes"].merge(id: ids.first)
41
+ model.changes_applied # loaded values are not user-supplied changes
42
+ load_versions(model, record)
43
+ set_previous_attributes(model, record)
44
+ model.instance_variable_set :@created_at, Time.zone.parse(record["created_at"])
45
+ model.instance_variable_set :@updated_at, Time.zone.parse(record["updated_at"])
46
+ model
47
+ else
48
+ results = ids.flatten.map do |id|
49
+ find(id)
50
+ rescue AllFutures::RecordNotFound
51
+ nil
52
+ end
53
+ return results if results.size == results.compact.size
54
+ raise AllFutures::RecordNotFound.new("Couldn't find all #{name.pluralize} with ids: #{ids.flatten.join(", ")} (found #{results.compact.size} results, but was looking for #{results.size})")
55
+ end
56
+ end
57
+
58
+ def find_by(attrs = {})
59
+ result = where(attrs)
60
+ result.any? ? result.first : nil
61
+ end
62
+
63
+ def find_by!(attrs = {})
64
+ result = find_by(attrs)
65
+ result.present? ? result : raise(AllFutures::RecordNotFound.new("Couldn't find #{name} with #{_pretty_attrs(attrs)}"))
66
+ end
67
+
68
+ def find_or_create_by(attrs = {}, &block)
69
+ return create({}, &block) unless attrs.present?
70
+ find_by!(attrs)
71
+ rescue AllFutures::RecordNotFound
72
+ create(attrs, &block)
73
+ end
74
+
75
+ def find_or_initialize_by(attrs = {}, &block)
76
+ return new({}, &block) unless attrs.present?
77
+ find_by!(attrs)
78
+ rescue AllFutures::RecordNotFound
79
+ new(attrs, &block)
80
+ end
81
+
82
+ def find_sole_by(attrs = {}, &block)
83
+ found, undesired = where(attrs, &block).first(2)
84
+
85
+ if found.nil?
86
+ raise AllFutures::RecordNotFound.new("Couldn't find #{name} record")
87
+ elsif undesired.present?
88
+ raise AllFutures::SoleRecordExceeded.new("Found multiple #{name} records when expecting only one")
89
+ else
90
+ found
91
+ end
92
+ end
93
+
94
+ def first(limit = nil)
95
+ limit ? all.first(limit) : all.first
96
+ end
97
+
98
+ def first!
99
+ first || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
100
+ end
101
+
102
+ def forty_two
103
+ find_nth 41
104
+ end
105
+
106
+ def forty_two!
107
+ forty_two || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
108
+ end
109
+
110
+ def fourth
111
+ find_nth 3
112
+ end
113
+
114
+ def fourth!
115
+ fourth || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
116
+ end
117
+
118
+ def ids
119
+ Kredis.redis.keys("#{name}:*").map { |id| id.delete_prefix("#{name}:") }
120
+ end
121
+
122
+ def include?(record)
123
+ record.is_a?(self) && exists?(record.id)
124
+ end
125
+ alias_method :member?, :include?
126
+
127
+ def last(limit = nil)
128
+ limit ? all.last(limit) : all.last
129
+ end
130
+
131
+ def last!
132
+ last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
133
+ end
134
+
135
+ def second
136
+ find_nth 1
137
+ end
138
+
139
+ def second!
140
+ second || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
141
+ end
142
+
143
+ def second_to_last
144
+ find_nth_from_last 1
145
+ end
146
+
147
+ def second_to_last!
148
+ second_to_last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
149
+ end
150
+
151
+ def sole
152
+ found, undesired = first(2)
153
+
154
+ if found.nil?
155
+ raise AllFutures::RecordNotFound.new("Couldn't find #{name} record")
156
+ elsif undesired.present?
157
+ raise AllFutures::SoleRecordExceeded.new("Found multiple #{name} records when expecting only one")
158
+ else
159
+ found
160
+ end
161
+ end
162
+
163
+ def third
164
+ find_nth 2
165
+ end
166
+
167
+ def third!
168
+ third || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
169
+ end
170
+
171
+ def third_to_last
172
+ find_nth_from_last 2
173
+ end
174
+
175
+ def third_to_last!
176
+ third_to_last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
177
+ end
178
+
179
+ def where(attrs = {}, &block)
180
+ if block.blank?
181
+ return all if attrs.blank?
182
+ begin
183
+ return [find(attrs.values.first)] if attrs.one? && attrs.keys.first == :id
184
+ rescue AllFutures::RecordNotFound
185
+ return []
186
+ end
187
+ end
188
+
189
+ attrs.each_key { |key| raise AllFutures::InvalidAttribute.new("#{key} is not a valid attribute") unless valid_attribute?(key) }
190
+ all.select do |record|
191
+ attrs.all? do |(attribute, value)|
192
+ record.send(attribute) == value.to_s
193
+ end && (block ? block.call(record) : true)
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ def find_nth(index)
200
+ records = all
201
+ (records.size >= index) ? records[index] : nil
202
+ end
203
+
204
+ def find_nth_from_last(index)
205
+ records = all.reverse
206
+ (records.size >= index) ? records[index] : nil
207
+ end
208
+
209
+ def _pretty_attrs(attrs)
210
+ attrs.map { |key, value| "#{key}: #{value}" }.join(", ")
211
+ end
212
+ end
213
+ end
214
+ end