• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Dynamoid / dynamoid / 4031642490

pending completion
4031642490

push

github

Andrew Konchin
add sti_name support

743 of 832 branches covered (89.3%)

Branch coverage included in aggregate %.

15 of 15 new or added lines in 4 files covered. (100.0%)

2796 of 3100 relevant lines covered (90.19%)

750.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.11
/lib/dynamoid/document.rb
1
# frozen_string_literal: true
2

3
module Dynamoid
1✔
4
  # This is the base module for all domain objects that need to be persisted to
5
  # the database as documents.
6
  module Document
1✔
7
    extend ActiveSupport::Concern
1✔
8
    include Dynamoid::Components
1✔
9

10
    included do
1✔
11
      class_attribute :options, :read_only_attributes, :base_class, instance_accessor: false
879✔
12
      self.options = {}
879✔
13
      self.read_only_attributes = []
879✔
14
      self.base_class = self
879✔
15

16
      Dynamoid.included_models << self unless Dynamoid.included_models.include? self
879!
17
    end
18

19
    module ClassMethods
1✔
20
      def attr_readonly(*read_only_attributes)
1✔
21
        self.read_only_attributes.concat read_only_attributes.map(&:to_s)
×
22
      end
23

24
      # Returns the read capacity for this table.
25
      #
26
      # @return [Integer] read capacity units
27
      # @since 0.4.0
28
      def read_capacity
1✔
29
        options[:read_capacity] || Dynamoid::Config.read_capacity
2,722✔
30
      end
31

32
      # Returns the write_capacity for this table.
33
      #
34
      # @return [Integer] write capacity units
35
      # @since 0.4.0
36
      def write_capacity
1✔
37
        options[:write_capacity] || Dynamoid::Config.write_capacity
2,722✔
38
      end
39

40
      # Returns the billing (capacity) mode for this table.
41
      #
42
      # Could be either +provisioned+ or +on_demand+.
43
      #
44
      # @return [Symbol]
45
      def capacity_mode
1✔
46
        options[:capacity_mode] || Dynamoid::Config.capacity_mode
2,720✔
47
      end
48

49
      # Returns the field name used to support STI for this table.
50
      #
51
      # Default field name is +type+ but it can be overrided in the +table+
52
      # method call.
53
      #
54
      #   User.inheritance_field # => :type
55
      def inheritance_field
1✔
56
        options[:inheritance_field] || :type
7,091✔
57
      end
58

59
      # Returns the hash key field name for this class.
60
      #
61
      # By default +id+ field is used. But it can be overriden in the +table+
62
      # method call.
63
      #
64
      #   User.hash_key # => :id
65
      #
66
      # @return [Symbol] a hash key name
67
      # @since 0.4.0
68
      def hash_key
1✔
69
        options[:key] || :id
58,483✔
70
      end
71

72
      # Return the count of items for this class.
73
      #
74
      # It returns approximate value based on DynamoDB statistic. DynamoDB
75
      # updates it periodically so the value can be no accurate.
76
      #
77
      # It's a reletively cheap operation and doesn't read all the items in a
78
      # table. It makes just one HTTP request to DynamoDB.
79
      #
80
      # @return [Integer] items count in a table
81
      # @since 0.6.1
82
      def count
1✔
83
        Dynamoid.adapter.count(table_name)
33✔
84
      end
85

86
      # Initialize a new object.
87
      #
88
      #   User.build(name: 'A')
89
      #
90
      # Initialize an object and pass it into a block to set other attributes.
91
      #
92
      #   User.build(name: 'A') do |u|
93
      #     u.age = 21
94
      #   end
95
      #
96
      # The only difference between +build+ and +new+ methods is that +build+
97
      # supports STI (Single table inheritance) and looks at the inheritance
98
      # field. So it can build a model of actual class. For instance:
99
      #
100
      #   class Employee
101
      #     include Dynamoid::Document
102
      #
103
      #     field :type
104
      #     field :name
105
      #   end
106
      #
107
      #   class Manager < Employee
108
      #   end
109
      #
110
      #   Employee.build(name: 'Alice', type: 'Manager') # => #<Manager:0x00007f945756e3f0 ...>
111
      #
112
      # @param attrs [Hash] Attributes with which to create the document
113
      # @param block [Proc] Block to process a document after initialization
114
      # @return [Dynamoid::Document] the new document
115
      # @since 0.2.0
116
      def build(attrs = {}, &block)
1✔
117
        choose_right_class(attrs).new(attrs, &block)
2,034✔
118
      end
119

120
      # Does this model exist in a table?
121
      #
122
      #   User.exists?('713') # => true
123
      #
124
      # If a range key is declared it should be specified in the following way:
125
      #
126
      #   User.exists?([['713', 'range-key-value']]) # => true
127
      #
128
      # It's possible to check existence of several models at once:
129
      #
130
      #   User.exists?(['713', '714', '715'])
131
      #
132
      # Or in case when a range key is declared:
133
      #
134
      #   User.exists?(
135
      #     [
136
      #       ['713', 'range-key-value-1'],
137
      #       ['714', 'range-key-value-2'],
138
      #       ['715', 'range-key-value-3']
139
      #     ]
140
      #   )
141
      #
142
      # It's also possible to specify models not with primary key but with
143
      # conditions on the attributes (in the +where+ method style):
144
      #
145
      #   User.exists?(age: 20, 'created_at.gt': Time.now - 1.day)
146
      #
147
      # @param id_or_conditions [String|Array[String]|Array[Array]|Hash] the primary id of the model, a list of primary ids or a hash with the options to filter from.
148
      # @return [true|false]
149
      # @since 0.2.0
150
      def exists?(id_or_conditions = {})
1✔
151
        case id_or_conditions
11✔
152
        when Hash then where(id_or_conditions).count >= 1
4✔
153
        else
154
          begin
7✔
155
            find(id_or_conditions)
7✔
156
            true
4✔
157
          rescue Dynamoid::Errors::RecordNotFound
3✔
158
            false
3✔
159
          end
160
        end
161
      end
162

163
      attr_accessor :abstract_class
1✔
164

165
      def abstract_class?
1✔
166
        defined?(@abstract_class) && @abstract_class == true
3,493✔
167
      end
168

169
      def sti_name
1✔
170
        name
45✔
171
      end
172

173
      def sti_class_for(type_name)
1✔
174
        type_name.constantize
12✔
175
      rescue NameError
176
        raise SubclassNotFound, "STI subclass does not found. Subclass: '#{type_name}'"
×
177
      end
178

179
      # @private
180
      def deep_subclasses
1✔
181
        subclasses + subclasses.map(&:deep_subclasses).flatten
32✔
182
      end
183

184
      # @private
185
      def choose_right_class(attrs)
1✔
186
        attrs[inheritance_field] ? sti_class_for(attrs[inheritance_field]) : self
3,182✔
187
      end
188
    end
189

190
    # Initialize a new object.
191
    #
192
    #   User.new(name: 'A')
193
    #
194
    # Initialize an object and pass it into a block to set other attributes.
195
    #
196
    #   User.new(name: 'A') do |u|
197
    #     u.age = 21
198
    #   end
199
    #
200
    # @param attrs [Hash] Attributes with which to create the document
201
    # @param block [Proc] Block to process a document after initialization
202
    # @return [Dynamoid::Document] the new document
203
    #
204
    # @since 0.2.0
205
    def initialize(attrs = {}, &block)
1✔
206
      run_callbacks :initialize do
3,482✔
207
        @new_record = true
3,482✔
208
        @attributes ||= {}
3,482✔
209
        @associations ||= {}
3,482✔
210
        @attributes_before_type_cast ||= {}
3,482✔
211

212
        attrs_with_defaults = self.class.attributes.each_with_object({}) do |(attribute, options), res|
3,482✔
213
          if attrs.key?(attribute)
25,834✔
214
            res[attribute] = attrs[attribute]
9,093✔
215
          elsif options.key?(:default)
16,741✔
216
            res[attribute] = evaluate_default_value(options[:default])
46✔
217
          end
218
        end
219

220
        attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
3,482✔
221

222
        load(attrs_with_defaults.merge(attrs_virtual))
3,482✔
223

224
        if block
3,481✔
225
          yield(self)
7✔
226
        end
227
      end
228
    end
229

230
    # Check equality of two models.
231
    #
232
    # A model is equal to another model only if their primary keys (hash key
233
    # and optionally range key) are equal.
234
    #
235
    # @return [true|false]
236
    # @since 0.2.0
237
    def ==(other)
1✔
238
      if self.class.identity_map_on?
20,689!
239
        super
×
240
      else
20,689✔
241
        return false if other.nil?
20,689✔
242

243
        other.is_a?(Dynamoid::Document) && hash_key == other.hash_key && range_value == other.range_value
20,670✔
244
      end
245
    end
246

247
    # Check equality of two models.
248
    #
249
    # Works exactly like +==+ does.
250
    #
251
    # @return [true|false]
252
    def eql?(other)
1✔
253
      self == other
21✔
254
    end
255

256
    # Generate an Integer hash value for this model.
257
    #
258
    # Hash value is based on primary key. So models can be used safely as a
259
    # +Hash+ keys.
260
    #
261
    # @return [Integer]
262
    def hash
1✔
263
      hash_key.hash ^ range_value.hash
120✔
264
    end
265

266
    # Return a model's hash key value.
267
    #
268
    # @since 0.4.0
269
    def hash_key
1✔
270
      self[self.class.hash_key.to_sym]
44,528✔
271
    end
272

273
    # Assign a model's hash key value, regardless of what it might be called to
274
    # the object.
275
    #
276
    # @since 0.4.0
277
    def hash_key=(value)
1✔
278
      self[self.class.hash_key.to_sym] = value
1,291✔
279
    end
280

281
    # Return a model's range key value.
282
    #
283
    # Returns +nil+ if a range key isn't declared for a model.
284
    def range_value
1✔
285
      if self.class.range_key
1,065✔
286
        self[self.class.range_key.to_sym]
292✔
287
      end
288
    end
289

290
    # Assign a model's range key value.
291
    def range_value=(value)
1✔
292
      if self.class.range_key
2!
293
        self[self.class.range_key.to_sym] = value
2✔
294
      end
295
    end
296

297
    private
1✔
298

299
    def dumped_range_value
1✔
300
      Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
×
301
    end
302

303
    # Evaluates the default value given, this is used by undump
304
    # when determining the value of the default given for a field options.
305
    #
306
    # @param val [Object] the attribute's default value
307
    def evaluate_default_value(val)
1✔
308
      if val.respond_to?(:call)
46✔
309
        val.call
9✔
310
      elsif val.duplicable?
37✔
311
        val.dup
37✔
312
      else
×
313
        val
×
314
      end
315
    end
316
  end
317
end
318

319
ActiveSupport.run_load_hooks(:dynamoid, Dynamoid::Document)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc