dynamoid 3.3.0 → 3.7.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 +4 -4
- data/CHANGELOG.md +104 -1
- data/README.md +146 -52
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +20 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +10 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +68 -23
- data/lib/dynamoid/associations/single_association.rb +31 -4
- data/lib/dynamoid/components.rb +2 -0
- data/lib/dynamoid/config.rb +15 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +9 -1
- data/lib/dynamoid/criteria/chain.rb +421 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +119 -64
- data/lib/dynamoid/document.rb +133 -46
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +251 -39
- data/lib/dynamoid/fields/declare.rb +86 -0
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +86 -17
- data/lib/dynamoid/loadable.rb +2 -2
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +502 -104
- data/lib/dynamoid/persistence/import.rb +2 -1
- data/lib/dynamoid/persistence/save.rb +1 -0
- data/lib/dynamoid/persistence/update_fields.rb +5 -2
- data/lib/dynamoid/persistence/update_validations.rb +18 -0
- data/lib/dynamoid/persistence/upsert.rb +5 -3
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +6 -1
- data/lib/dynamoid/version.rb +1 -1
- metadata +48 -75
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -44
- data/Appraisals +0 -22
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -9
- data/gemfiles/rails_5_0.gemfile +0 -8
- data/gemfiles/rails_5_1.gemfile +0 -8
- data/gemfiles/rails_5_2.gemfile +0 -8
- data/gemfiles/rails_6_0.gemfile +0 -8
| @@ -2,6 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Dynamoid
         | 
| 4 4 | 
             
              module Criteria
         | 
| 5 | 
            +
                # @private
         | 
| 5 6 | 
             
                class IgnoredConditionsDetector
         | 
| 6 7 | 
             
                  def initialize(conditions)
         | 
| 7 8 | 
             
                    @conditions = conditions
         | 
| @@ -24,8 +25,8 @@ module Dynamoid | |
| 24 25 | 
             
                  def ignored_keys
         | 
| 25 26 | 
             
                    @conditions.keys
         | 
| 26 27 | 
             
                      .group_by(&method(:key_to_field))
         | 
| 27 | 
            -
                      .select { | | 
| 28 | 
            -
                      .flat_map { | | 
| 28 | 
            +
                      .select { |_, ary| ary.size > 1 }
         | 
| 29 | 
            +
                      .flat_map { |_, ary| ary[0..-2] }
         | 
| 29 30 | 
             
                  end
         | 
| 30 31 |  | 
| 31 32 | 
             
                  def key_to_field(key)
         | 
| @@ -38,4 +39,3 @@ module Dynamoid | |
| 38 39 | 
             
                end
         | 
| 39 40 | 
             
              end
         | 
| 40 41 | 
             
            end
         | 
| 41 | 
            -
             | 
| @@ -1,9 +1,9 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            module Dynamoid | 
| 3 | 
            +
            module Dynamoid
         | 
| 4 4 | 
             
              module Criteria
         | 
| 5 | 
            +
                # @private
         | 
| 5 6 | 
             
                class KeyFieldsDetector
         | 
| 6 | 
            -
             | 
| 7 7 | 
             
                  class Query
         | 
| 8 8 | 
             
                    def initialize(query_hash)
         | 
| 9 9 | 
             
                      @query_hash = query_hash
         | 
| @@ -11,6 +11,10 @@ module Dynamoid #:nodoc: | |
| 11 11 | 
             
                      @fields = query_hash.keys.map(&:to_s).map { |s| s.split('.').first }
         | 
| 12 12 | 
             
                    end
         | 
| 13 13 |  | 
| 14 | 
            +
                    def contain_only?(field_names)
         | 
| 15 | 
            +
                      (@fields - field_names.map(&:to_s)).blank?
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 14 18 | 
             
                    def contain?(field_name)
         | 
| 15 19 | 
             
                      @fields.include?(field_name.to_s)
         | 
| 16 20 | 
             
                    end
         | 
| @@ -20,13 +24,18 @@ module Dynamoid #:nodoc: | |
| 20 24 | 
             
                    end
         | 
| 21 25 | 
             
                  end
         | 
| 22 26 |  | 
| 23 | 
            -
                  def initialize(query, source)
         | 
| 27 | 
            +
                  def initialize(query, source, forced_index_name: nil)
         | 
| 24 28 | 
             
                    @query = query
         | 
| 25 29 | 
             
                    @source = source
         | 
| 26 30 | 
             
                    @query = Query.new(query)
         | 
| 31 | 
            +
                    @forced_index_name = forced_index_name
         | 
| 27 32 | 
             
                    @result = find_keys_in_query
         | 
| 28 33 | 
             
                  end
         | 
| 29 34 |  | 
| 35 | 
            +
                  def non_key_present?
         | 
| 36 | 
            +
                    !@query.contain_only?([hash_key, range_key].compact)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 30 39 | 
             
                  def key_present?
         | 
| 31 40 | 
             
                    @result.present?
         | 
| 32 41 | 
             
                  end
         | 
| @@ -46,6 +55,8 @@ module Dynamoid #:nodoc: | |
| 46 55 | 
             
                  private
         | 
| 47 56 |  | 
| 48 57 | 
             
                  def find_keys_in_query
         | 
| 58 | 
            +
                    return match_forced_index if @forced_index_name
         | 
| 59 | 
            +
             | 
| 49 60 | 
             
                    match_table_and_sort_key ||
         | 
| 50 61 | 
             
                      match_local_secondary_index ||
         | 
| 51 62 | 
             
                      match_global_secondary_index_and_sort_key ||
         | 
| @@ -71,8 +82,8 @@ module Dynamoid #:nodoc: | |
| 71 82 | 
             
                  def match_local_secondary_index
         | 
| 72 83 | 
             
                    return unless @query.contain_with_eq_operator?(@source.hash_key)
         | 
| 73 84 |  | 
| 74 | 
            -
                    lsi = @source.local_secondary_indexes.values.find do | | 
| 75 | 
            -
                      @query.contain?( | 
| 85 | 
            +
                    lsi = @source.local_secondary_indexes.values.find do |i|
         | 
| 86 | 
            +
                      @query.contain?(i.range_key)
         | 
| 76 87 | 
             
                    end
         | 
| 77 88 |  | 
| 78 89 | 
             
                    if lsi.present?
         | 
| @@ -90,9 +101,9 @@ module Dynamoid #:nodoc: | |
| 90 101 | 
             
                  # But only do so if projects ALL attributes otherwise we won't
         | 
| 91 102 | 
             
                  # get back full data
         | 
| 92 103 | 
             
                  def match_global_secondary_index_and_sort_key
         | 
| 93 | 
            -
                    gsi = @source.global_secondary_indexes.values.find do | | 
| 94 | 
            -
                      @query.contain_with_eq_operator?( | 
| 95 | 
            -
                        @query.contain?( | 
| 104 | 
            +
                    gsi = @source.global_secondary_indexes.values.find do |i|
         | 
| 105 | 
            +
                      @query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all &&
         | 
| 106 | 
            +
                        @query.contain?(i.range_key)
         | 
| 96 107 | 
             
                    end
         | 
| 97 108 |  | 
| 98 109 | 
             
                    if gsi.present?
         | 
| @@ -113,8 +124,8 @@ module Dynamoid #:nodoc: | |
| 113 124 | 
             
                  end
         | 
| 114 125 |  | 
| 115 126 | 
             
                  def match_global_secondary_index
         | 
| 116 | 
            -
                    gsi = @source.global_secondary_indexes.values.find do | | 
| 117 | 
            -
                      @query.contain_with_eq_operator?( | 
| 127 | 
            +
                    gsi = @source.global_secondary_indexes.values.find do |i|
         | 
| 128 | 
            +
                      @query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all
         | 
| 118 129 | 
             
                    end
         | 
| 119 130 |  | 
| 120 131 | 
             
                    if gsi.present?
         | 
| @@ -125,6 +136,16 @@ module Dynamoid #:nodoc: | |
| 125 136 | 
             
                      }
         | 
| 126 137 | 
             
                    end
         | 
| 127 138 | 
             
                  end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  def match_forced_index
         | 
| 141 | 
            +
                    idx = @source.find_index_by_name(@forced_index_name)
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    {
         | 
| 144 | 
            +
                      hash_key: idx.hash_key,
         | 
| 145 | 
            +
                      range_key: idx.range_key,
         | 
| 146 | 
            +
                      index_name: idx.name,
         | 
| 147 | 
            +
                    }
         | 
| 148 | 
            +
                  end
         | 
| 128 149 | 
             
                end
         | 
| 129 150 | 
             
              end
         | 
| 130 151 | 
             
            end
         | 
| @@ -2,6 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Dynamoid
         | 
| 4 4 | 
             
              module Criteria
         | 
| 5 | 
            +
                # @private
         | 
| 5 6 | 
             
                class NonexistentFieldsDetector
         | 
| 6 7 | 
             
                  def initialize(conditions, source)
         | 
| 7 8 | 
             
                    @conditions = conditions
         | 
| @@ -19,8 +20,8 @@ module Dynamoid | |
| 19 20 | 
             
                    fields_list = @nonexistent_fields.map { |s| "`#{s}`" }.join(', ')
         | 
| 20 21 | 
             
                    count = @nonexistent_fields.size
         | 
| 21 22 |  | 
| 22 | 
            -
                     | 
| 23 | 
            -
                      " field #{ | 
| 23 | 
            +
                    'where conditions contain nonexistent' \
         | 
| 24 | 
            +
                      " field #{'name'.pluralize(count)} #{fields_list}"
         | 
| 24 25 | 
             
                  end
         | 
| 25 26 |  | 
| 26 27 | 
             
                  private
         | 
    
        data/lib/dynamoid/dirty.rb
    CHANGED
    
    | @@ -22,6 +22,7 @@ module Dynamoid | |
| 22 22 | 
             
                  attribute_method_affix prefix: 'restore_', suffix: '!'
         | 
| 23 23 | 
             
                end
         | 
| 24 24 |  | 
| 25 | 
            +
                # @private
         | 
| 25 26 | 
             
                module ClassMethods
         | 
| 26 27 | 
             
                  def update_fields(*)
         | 
| 27 28 | 
             
                    if model = super
         | 
| @@ -44,6 +45,7 @@ module Dynamoid | |
| 44 45 | 
             
                  end
         | 
| 45 46 | 
             
                end
         | 
| 46 47 |  | 
| 48 | 
            +
                # @private
         | 
| 47 49 | 
             
                def save(*)
         | 
| 48 50 | 
             
                  if status = super
         | 
| 49 51 | 
             
                    changes_applied
         | 
| @@ -51,24 +53,28 @@ module Dynamoid | |
| 51 53 | 
             
                  status
         | 
| 52 54 | 
             
                end
         | 
| 53 55 |  | 
| 56 | 
            +
                # @private
         | 
| 54 57 | 
             
                def save!(*)
         | 
| 55 58 | 
             
                  super.tap do
         | 
| 56 59 | 
             
                    changes_applied
         | 
| 57 60 | 
             
                  end
         | 
| 58 61 | 
             
                end
         | 
| 59 62 |  | 
| 63 | 
            +
                # @private
         | 
| 60 64 | 
             
                def update(*)
         | 
| 61 65 | 
             
                  super.tap do
         | 
| 62 66 | 
             
                    clear_changes_information
         | 
| 63 67 | 
             
                  end
         | 
| 64 68 | 
             
                end
         | 
| 65 69 |  | 
| 70 | 
            +
                # @private
         | 
| 66 71 | 
             
                def update!(*)
         | 
| 67 72 | 
             
                  super.tap do
         | 
| 68 73 | 
             
                    clear_changes_information
         | 
| 69 74 | 
             
                  end
         | 
| 70 75 | 
             
                end
         | 
| 71 76 |  | 
| 77 | 
            +
                # @private
         | 
| 72 78 | 
             
                def reload(*)
         | 
| 73 79 | 
             
                  super.tap do
         | 
| 74 80 | 
             
                    clear_changes_information
         | 
| @@ -78,17 +84,22 @@ module Dynamoid | |
| 78 84 | 
             
                # Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
         | 
| 79 85 | 
             
                #
         | 
| 80 86 | 
             
                #   person.changed? # => false
         | 
| 81 | 
            -
                #   person.name = ' | 
| 87 | 
            +
                #   person.name = 'Bob'
         | 
| 82 88 | 
             
                #   person.changed? # => true
         | 
| 89 | 
            +
                #
         | 
| 90 | 
            +
                # @return [true|false]
         | 
| 83 91 | 
             
                def changed?
         | 
| 84 92 | 
             
                  changed_attributes.present?
         | 
| 85 93 | 
             
                end
         | 
| 86 94 |  | 
| 87 | 
            -
                # Returns an array with  | 
| 95 | 
            +
                # Returns an array with names of the attributes with unsaved changes.
         | 
| 88 96 | 
             
                #
         | 
| 97 | 
            +
                #   person = Person.new
         | 
| 89 98 | 
             
                #   person.changed # => []
         | 
| 90 | 
            -
                #   person.name = ' | 
| 99 | 
            +
                #   person.name = 'Bob'
         | 
| 91 100 | 
             
                #   person.changed # => ["name"]
         | 
| 101 | 
            +
                #
         | 
| 102 | 
            +
                # @return [Array[String]]
         | 
| 92 103 | 
             
                def changed
         | 
| 93 104 | 
             
                  changed_attributes.keys
         | 
| 94 105 | 
             
                end
         | 
| @@ -97,18 +108,22 @@ module Dynamoid | |
| 97 108 | 
             
                # and new values like <tt>attr => [original value, new value]</tt>.
         | 
| 98 109 | 
             
                #
         | 
| 99 110 | 
             
                #   person.changes # => {}
         | 
| 100 | 
            -
                #   person.name = ' | 
| 101 | 
            -
                #   person.changes # => { "name" => [" | 
| 111 | 
            +
                #   person.name = 'Bob'
         | 
| 112 | 
            +
                #   person.changes # => { "name" => ["Bill", "Bob"] }
         | 
| 113 | 
            +
                #
         | 
| 114 | 
            +
                # @return [ActiveSupport::HashWithIndifferentAccess]
         | 
| 102 115 | 
             
                def changes
         | 
| 103 116 | 
             
                  ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
         | 
| 104 117 | 
             
                end
         | 
| 105 118 |  | 
| 106 119 | 
             
                # Returns a hash of attributes that were changed before the model was saved.
         | 
| 107 120 | 
             
                #
         | 
| 108 | 
            -
                #   person.name # => " | 
| 109 | 
            -
                #   person.name = ' | 
| 121 | 
            +
                #   person.name # => "Bob"
         | 
| 122 | 
            +
                #   person.name = 'Robert'
         | 
| 110 123 | 
             
                #   person.save
         | 
| 111 | 
            -
                #   person.previous_changes # => {"name" => [" | 
| 124 | 
            +
                #   person.previous_changes # => {"name" => ["Bob", "Robert"]}
         | 
| 125 | 
            +
                #
         | 
| 126 | 
            +
                # @return [ActiveSupport::HashWithIndifferentAccess]
         | 
| 112 127 | 
             
                def previous_changes
         | 
| 113 128 | 
             
                  @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
         | 
| 114 129 | 
             
                end
         | 
| @@ -116,15 +131,28 @@ module Dynamoid | |
| 116 131 | 
             
                # Returns a hash of the attributes with unsaved changes indicating their original
         | 
| 117 132 | 
             
                # values like <tt>attr => original value</tt>.
         | 
| 118 133 | 
             
                #
         | 
| 119 | 
            -
                #   person.name # => " | 
| 120 | 
            -
                #   person.name = ' | 
| 121 | 
            -
                #   person.changed_attributes # => {"name" => " | 
| 134 | 
            +
                #   person.name # => "Bob"
         | 
| 135 | 
            +
                #   person.name = 'Robert'
         | 
| 136 | 
            +
                #   person.changed_attributes # => {"name" => "Bob"}
         | 
| 137 | 
            +
                #
         | 
| 138 | 
            +
                # @return [ActiveSupport::HashWithIndifferentAccess]
         | 
| 122 139 | 
             
                def changed_attributes
         | 
| 123 140 | 
             
                  @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
         | 
| 124 141 | 
             
                end
         | 
| 125 142 |  | 
| 126 143 | 
             
                # Handle <tt>*_changed?</tt> for +method_missing+.
         | 
| 127 | 
            -
                 | 
| 144 | 
            +
                #
         | 
| 145 | 
            +
                #  person.attribute_changed?(:name) # => true
         | 
| 146 | 
            +
                #  person.attribute_changed?(:name, from: 'Alice')
         | 
| 147 | 
            +
                #  person.attribute_changed?(:name, to: 'Bob')
         | 
| 148 | 
            +
                #  person.attribute_changed?(:name, from: 'Alice', to: 'Bod')
         | 
| 149 | 
            +
                #
         | 
| 150 | 
            +
                # @private
         | 
| 151 | 
            +
                # @param attr [Symbol] attribute name
         | 
| 152 | 
            +
                # @param options [Hash] conditions on +from+ and +to+ value (optional)
         | 
| 153 | 
            +
                # @option options [Symbol] :from previous attribute value
         | 
| 154 | 
            +
                # @option options [Symbol] :to current attribute value
         | 
| 155 | 
            +
                def attribute_changed?(attr, options = {})
         | 
| 128 156 | 
             
                  result = changes_include?(attr)
         | 
| 129 157 | 
             
                  result &&= options[:to] == __send__(attr) if options.key?(:to)
         | 
| 130 158 | 
             
                  result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
         | 
| @@ -132,88 +160,115 @@ module Dynamoid | |
| 132 160 | 
             
                end
         | 
| 133 161 |  | 
| 134 162 | 
             
                # Handle <tt>*_was</tt> for +method_missing+.
         | 
| 135 | 
            -
                 | 
| 163 | 
            +
                #
         | 
| 164 | 
            +
                #  person = Person.create(name: 'Alice')
         | 
| 165 | 
            +
                #  person.name = 'Bob'
         | 
| 166 | 
            +
                #  person.attribute_was(:name) # => "Alice"
         | 
| 167 | 
            +
                #
         | 
| 168 | 
            +
                # @private
         | 
| 169 | 
            +
                # @param attr [Symbol] attribute name
         | 
| 170 | 
            +
                def attribute_was(attr)
         | 
| 136 171 | 
             
                  attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
         | 
| 137 172 | 
             
                end
         | 
| 138 173 |  | 
| 139 174 | 
             
                # Restore all previous data of the provided attributes.
         | 
| 175 | 
            +
                #
         | 
| 176 | 
            +
                # @param attributes [Array[Symbol]] a list of attribute names
         | 
| 140 177 | 
             
                def restore_attributes(attributes = changed)
         | 
| 141 178 | 
             
                  attributes.each { |attr| restore_attribute! attr }
         | 
| 142 179 | 
             
                end
         | 
| 143 180 |  | 
| 144 181 | 
             
                # Handles <tt>*_previously_changed?</tt> for +method_missing+.
         | 
| 145 | 
            -
                 | 
| 182 | 
            +
                #
         | 
| 183 | 
            +
                #  person = Person.create(name: 'Alice')
         | 
| 184 | 
            +
                #  person.name = 'Bob'
         | 
| 185 | 
            +
                #  person.save
         | 
| 186 | 
            +
                #  person.attribute_changed?(:name) # => true
         | 
| 187 | 
            +
                #
         | 
| 188 | 
            +
                # @private
         | 
| 189 | 
            +
                # @param attr [Symbol] attribute name
         | 
| 190 | 
            +
                # @return [true|false]
         | 
| 191 | 
            +
                def attribute_previously_changed?(attr)
         | 
| 146 192 | 
             
                  previous_changes_include?(attr)
         | 
| 147 193 | 
             
                end
         | 
| 148 194 |  | 
| 149 195 | 
             
                # Handles <tt>*_previous_change</tt> for +method_missing+.
         | 
| 196 | 
            +
                #
         | 
| 197 | 
            +
                #  person = Person.create(name: 'Alice')
         | 
| 198 | 
            +
                #  person.name = 'Bob'
         | 
| 199 | 
            +
                #  person.save
         | 
| 200 | 
            +
                #  person.attribute_previously_changed(:name) # => ["Alice", "Bob"]
         | 
| 201 | 
            +
                #
         | 
| 202 | 
            +
                # @private
         | 
| 203 | 
            +
                # @param attr [Symbol]
         | 
| 204 | 
            +
                # @return [Array]
         | 
| 150 205 | 
             
                def attribute_previous_change(attr)
         | 
| 151 206 | 
             
                  previous_changes[attr] if attribute_previously_changed?(attr)
         | 
| 152 207 | 
             
                end
         | 
| 153 208 |  | 
| 154 209 | 
             
                private
         | 
| 155 210 |  | 
| 156 | 
            -
             | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 161 | 
            -
                  # Removes current changes and makes them accessible through +previous_changes+.
         | 
| 162 | 
            -
                  def changes_applied # :doc:
         | 
| 163 | 
            -
                    @previously_changed = changes
         | 
| 164 | 
            -
                    @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
         | 
| 165 | 
            -
                  end
         | 
| 211 | 
            +
                def changes_include?(attr_name)
         | 
| 212 | 
            +
                  attributes_changed_by_setter.include?(attr_name)
         | 
| 213 | 
            +
                end
         | 
| 214 | 
            +
                alias attribute_changed_by_setter? changes_include?
         | 
| 166 215 |  | 
| 167 | 
            -
             | 
| 168 | 
            -
             | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 216 | 
            +
                # Removes current changes and makes them accessible through +previous_changes+.
         | 
| 217 | 
            +
                def changes_applied # :doc:
         | 
| 218 | 
            +
                  @previously_changed = changes
         | 
| 219 | 
            +
                  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
         | 
| 220 | 
            +
                end
         | 
| 172 221 |  | 
| 173 | 
            -
             | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
                   | 
| 222 | 
            +
                # Clear all dirty data: current changes and previous changes.
         | 
| 223 | 
            +
                def clear_changes_information # :doc:
         | 
| 224 | 
            +
                  @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
         | 
| 225 | 
            +
                  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
         | 
| 226 | 
            +
                end
         | 
| 177 227 |  | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 180 | 
            -
             | 
| 228 | 
            +
                # Handle <tt>*_change</tt> for +method_missing+.
         | 
| 229 | 
            +
                def attribute_change(attr)
         | 
| 230 | 
            +
                  [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
         | 
| 231 | 
            +
                end
         | 
| 181 232 |  | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
                    rescue TypeError, NoMethodError
         | 
| 186 | 
            -
                    end
         | 
| 233 | 
            +
                # Handle <tt>*_will_change!</tt> for +method_missing+.
         | 
| 234 | 
            +
                def attribute_will_change!(attr)
         | 
| 235 | 
            +
                  return if attribute_changed?(attr)
         | 
| 187 236 |  | 
| 188 | 
            -
             | 
| 237 | 
            +
                  begin
         | 
| 238 | 
            +
                    value = __send__(attr)
         | 
| 239 | 
            +
                    value = value.duplicable? ? value.clone : value
         | 
| 240 | 
            +
                  rescue TypeError, NoMethodError
         | 
| 189 241 | 
             
                  end
         | 
| 190 242 |  | 
| 191 | 
            -
                   | 
| 192 | 
            -
             | 
| 193 | 
            -
                    if attribute_changed?(attr)
         | 
| 194 | 
            -
                      __send__("#{attr}=", changed_attributes[attr])
         | 
| 195 | 
            -
                      clear_attribute_changes([attr])
         | 
| 196 | 
            -
                    end
         | 
| 197 | 
            -
                  end
         | 
| 243 | 
            +
                  set_attribute_was(attr, value)
         | 
| 244 | 
            +
                end
         | 
| 198 245 |  | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
                   | 
| 202 | 
            -
                     | 
| 246 | 
            +
                # Handle <tt>restore_*!</tt> for +method_missing+.
         | 
| 247 | 
            +
                def restore_attribute!(attr)
         | 
| 248 | 
            +
                  if attribute_changed?(attr)
         | 
| 249 | 
            +
                    __send__("#{attr}=", changed_attributes[attr])
         | 
| 250 | 
            +
                    clear_attribute_changes([attr])
         | 
| 203 251 | 
             
                  end
         | 
| 252 | 
            +
                end
         | 
| 204 253 |  | 
| 205 | 
            -
             | 
| 206 | 
            -
             | 
| 207 | 
            -
             | 
| 254 | 
            +
                # Returns +true+ if attr_name were changed before the model was saved,
         | 
| 255 | 
            +
                # +false+ otherwise.
         | 
| 256 | 
            +
                def previous_changes_include?(attr_name)
         | 
| 257 | 
            +
                  previous_changes.include?(attr_name)
         | 
| 258 | 
            +
                end
         | 
| 208 259 |  | 
| 209 | 
            -
             | 
| 210 | 
            -
             | 
| 211 | 
            -
             | 
| 212 | 
            -
                  end
         | 
| 260 | 
            +
                # This is necessary because `changed_attributes` might be overridden in
         | 
| 261 | 
            +
                # other implemntations (e.g. in `ActiveRecord`)
         | 
| 262 | 
            +
                alias attributes_changed_by_setter changed_attributes
         | 
| 213 263 |  | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 264 | 
            +
                # Force an attribute to have a particular "before" value
         | 
| 265 | 
            +
                def set_attribute_was(attr, old_value)
         | 
| 266 | 
            +
                  attributes_changed_by_setter[attr] = old_value
         | 
| 267 | 
            +
                end
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                # Remove changes information for the provided attributes.
         | 
| 270 | 
            +
                def clear_attribute_changes(attributes)
         | 
| 271 | 
            +
                  attributes_changed_by_setter.except!(*attributes)
         | 
| 272 | 
            +
                end
         | 
| 218 273 | 
             
              end
         | 
| 219 274 | 
             
            end
         |