File: attribute_collection.rb

package info (click to toggle)
ruby-aws-sdk 1.67.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,840 kB
  • sloc: ruby: 28,436; makefile: 7
file content (456 lines) | stat: -rw-r--r-- 16,784 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

module AWS
  class DynamoDB

    # Represents the attributes of a DynamoDB item.  An attribute is a
    # name-value pair. The name must be a string, but the value can be
    # a string, number, string set, or number set.  Attribute values
    # cannot be null or empty.
    #
    # @note The SDK always returns numbers as BigDecimal objects and
    #   sets as Set objects; however, on input it accepts any numeric
    #   type for number attributes and either Arrays or Sets for set
    #   attributes.
    #
    # @example Retrieving specific attributes of an item
    #   (title, description) =
    #     item.attributes.values_at(:title, :description)
    #
    # @example Retrieving all attributes in hash form
    #   item.attributes.to_hash
    #
    # @example Replacing the value of an attribute
    #   item.attributes[:title] = "Automobiles"
    #
    # @example Doing a mixed update of item attributes in a single operation
    #   item.attributes.update do |u|
    #
    #     # add 12 to the (numeric) value of "views"
    #     u.add(:views => 12)
    #
    #     # delete attributes
    #     u.delete(:foo, :bar)
    #
    #     # delete values out of a set attribute
    #     u.delete(:colors => ["red", "blue"])
    #
    #     # replace values
    #     u.set(:title => "More Automobiles")
    #   end
    #
    # @example Returning overwritten values
    #   item.attributes.to_hash      # => { "foo" => "bar",
    #                                    "name" => "fred" }
    #   item.attributes.set(
    #     { "foo" => "baz" },
    #     :return => :updated_old
    #   )                         # => { "foo" => "bar" }
    #
    # @example Performing a conditional update
    #   item.set({ :version => 3 }, :if => { :version => 2 })
    class AttributeCollection

      include Core::Model
      include Enumerable
      include Types
      include Keys
      include Expectations

      # @return [Item] The item to which these attributes belong.
      attr_reader :item

      # @api private
      def initialize(item, opts = {})
        @item = item
        super
      end

      # Behaves like Hash#each; yields each attribute as a name/value
      # pair.
      #
      #     attributes.each { |(name, value)| puts "#{name} = #{value}" }
      #
      #     attributes.each { |name, value| puts "#{name} = #{value}" }
      #
      # @param (see #to_hash)
      #
      # @option options (see #to_hash)
      def each(options = {}, &block)
        to_hash(options).each(&block)
      end

      # @yieldparam [String] name Each attribute name.
      def each_key(options = {})
        each(options) { |k, v| yield(k) if block_given? }
      end

      # @yieldparam value Each attribute value belonging to the item.
      #   Values will be of type String, BigDecimal, Set<String> or
      #   Set<BigDecimal>.
      def each_value(options = {})
        each(options) { |k, v| yield(v) if block_given? }
      end

      # Retrieves the value of a single attribute.
      #
      # @param [String, Symbol] attribute The name of the attribute to
      #   get.
      #
      # @return The value of the specified attribute, which may be a
      #   String, BigDecimal, Set<String> or Set<BigDecimal>.
      def [] attribute
        attribute = attribute.to_s
        response_attributes = get_item(:attributes_to_get => [attribute])
        value_from_response(response_attributes[attribute])
      end

      # Replaces the value of a single attribute.
      #
      # @param [String, Symbol] attribute The name of the attribute to
      #   replace.
      #
      # @param value The new value to set.  This may be a String,
      #   Numeric, Set or Array of String objects, or Set or Array of
      #   Numeric objects.  Mixed types are not allowed in sets, and
      #   neither String values nor set values may be empty.  Setting
      #   an attribute to nil is the same as deleting the attribute.
      #
      # @return The new value of the attribute.
      def []= attribute, value
        set(attribute => value)
        value
      end

      # Replaces the values of one or more attributes.
      #
      # @param [Hash] attributes The attributes to replace.  The keys
      #   of the hash may be strings or symbols.  The values may be of
      #   type String, Numeric, Set or Array of String objects, or Set
      #   or Array of Numeric objects.  Mixed types are not allowed in
      #   sets, and neither String values nor set values may be empty.
      #   Setting an attribute to nil is the same as deleting the
      #   attribute.
      #
      # @param [Hash] options Options for updating the item.
      #
      # @option options (see #update)
      def set attributes, options = {}
        update(options) { |u| u.set(attributes) }
      end
      alias_method :merge!, :set
      alias_method :put, :set

      # Adds to the values of one or more attributes.  Each attribute
      # must be a set or number in the original item, and each input
      # value must have the same type as the value in the original
      # item.  For example, it is invalid to add a single number to a
      # set of numbers, but it is valid to add a set containing a
      # single number to another set of numbers.
      #
      # When the original attribute is a set, the values provided to
      # this method are added to that set.  When the original
      # attribute is a number, the original value is incremented by
      # the numeric value provided to this method.  For example:
      #
      #     item = table.items.put(
      #       :id => "abc123",
      #       :colors => ["red", "white"],
      #       :age => 3
      #     )
      #     item.attributes.add(
      #       { :colors => ["muave"],
      #         :age => 1 },
      #       :return => :updated_new
      #     ) # => { "colors" => Set["red", "white", "mauve"], "age" => 4 }
      #
      # @param [Hash] attributes The attribute values to add.  The
      #   keys of the hash may be strings or symbols.  The values may
      #   be of type Numeric, Set or Array of String objects, or Set
      #   or Array of Numeric objects.  Mixed types are not allowed in
      #   sets, and neither String values nor set values may be empty.
      #   Single string values are not allowed for this method, since
      #   DynamoDB does not currently support adding a string to
      #   another string.
      #
      # @param [Hash] options Options for updating the item.
      #
      # @option options (see #update)
      def add attributes, options = {}
        update(options) { |u| u.add(attributes) }
      end

      # @overload delete *attributes
      #
      #   Deletes attributes from the item.  Each argument must be a
      #   string or symbol specifying the name of the attribute to
      #   delete.  The last argument may be a hash containing options
      #   for the update.  See {#update} for more information about
      #   what options are accepted.
      #
      # @overload delete attributes, options = {}
      #
      #   Deletes specific values from one or more attributes, whose
      #   original values must be sets.
      #
      #   @param [Hash] attributes The attribute values to delete.
      #     The keys of the hash may be strings or symbols.  The
      #     values must be arrays or Sets of numbers or strings.
      #     Mixed types are not allowed in sets.  The type of each
      #     member in a set must match the type of the members in the
      #     original attribute value.
      #
      #   @param [Hash] options Options for updating the item.
      #
      #   @option options (see #update)
      def delete *args
        if args.first.kind_of?(Hash)
          delete_args = [args.shift]
        else
          delete_args = args
        end
        options = args.pop if args.last.kind_of?(Hash)
        update(options || {}) { |u| u.delete(*delete_args) }
      end

      # Used to build a batch of updates to an item's attributes.  See
      # {AttributeCollection#update} for more information.
      class UpdateBuilder

        # @api private
        attr_reader :updates

        include Types

        # @api private
        def initialize
          @updates = {}
        end

        # Replaces the values of one or more attributes.  See
        # {AttributeCollection#set} for more information.
        def set attributes
          to_delete = []
          attributes = attributes.inject({}) do |attributes, (name, value)|
            if value == nil
              to_delete << name
            else
              attributes[name] = value
            end
            attributes
          end
          attribute_updates("PUT", attributes)
          delete(*to_delete)
        end
        alias_method :put, :set
        alias_method :merge!, :set

        # Adds to the values of one or more attributes.  See
        # {AttributeCollection#add} for more information.
        def add attributes
          attribute_updates("ADD", attributes)
        end


        # Deletes one or more attributes or attribute values.  See
        # {AttributeCollection#delete} for more information.
        def delete *args
          if args.first.kind_of?(Hash)
            attribute_updates("DELETE",
                              args.shift,
                              :setify => true)
          else
            add_updates(args.inject({}) do |u, name|
                          u.update(name.to_s => {
                                     :action => "DELETE"
                                   })
                        end)
          end
        end

        private
        def attribute_updates(action, attributes, our_opts = {})
          new_updates = attributes.inject({}) do |new_updates, (name, value)|
            name = name.to_s
            context = "in value for attribute #{name}"
            value = [value].flatten if our_opts[:setify]
            new_updates.update(name => {
                                 :action => action,
                                 :value =>
                                 format_attribute_value(value, context)
                               })
          end
          add_updates(new_updates)
        end

        private
        def add_updates(new_updates)
          updates.merge!(new_updates) do |name, old, new|
            raise ArgumentError, "conflicting updates for attribute #{name}"
          end
        end

      end

      # Updates multiple attributes in a single operation.  This is
      # more efficient than performing each type of update in
      # sequence, and it also allows you to group different kinds of
      # updates into an atomic operation.
      #
      #     item.attributes.update do |u|
      #
      #       # add 12 to the (numeric) value of "views"
      #       u.add(:views => 12)
      #
      #       # delete attributes
      #       u.delete(:foo, :bar)
      #
      #       # delete values out of a set attribute
      #       u.delete(:colors => ["red", "blue"])
      #
      #       # replace values
      #       u.set(:title => "More Automobiles")
      #     end
      #
      # @param [Hash] options Options for updating the item.
      #
      # @option options [Hash] :if Designates a conditional update.
      #   The operation will fail unless the item exists and has the
      #   attributes in the value for this option.  For example:
      #
      #       # throws DynamoDB::Errors::ConditionalCheckFailedException
      #       # unless the item has "color" set to "red"
      #       item.attributes.update(:if => { :color => "red" }) do |u|
      #         # ...
      #       end
      #
      # @option options [String, Symbol, Array] :unless_exists A name
      #   or collection of attribute names; if the item already exists
      #   and has a value for any of these attributes, this method
      #   will raise
      #   `DynamoDB::Errors::ConditionalCheckFailedException`.  For example:
      #
      #       item.attributes.update(:unless_exists => :color) do |u|
      #         # ...
      #       end
      #
      # @option options [Symbol] :return (`:none`) Changes the return
      #   value of the method.  Valid values:
      #
      #     * `:none` - Return `nil`
      #     * `:all_old` - Returns a hash containing all of the original
      #       values of the attributes before the update, or
      #       `nil` if the item did not exist at the time of
      #       the update.
      #     * `:updated_old` - Returns a hash containing the original
      #       values of the attributes that were modified
      #       as part of this operation.  This includes
      #       attributes that were deleted, and
      #       set-valued attributes whose member values
      #       were deleted.
      #     * `:updated_new` - Returns a hash containing the new values of
      #       the attributes that were modified as part
      #       of this operation.  This includes
      #       set-valued attributes whose member values
      #       were deleted.
      #     * `:all_new` - Returns a hash containing the new values of all
      #       of the attributes.
      #
      # @yieldparam [UpdateBuilder] builder A handle for describing
      #   the update.
      #
      # @note DnamoDB allows only one update per attribute in a
      #   single operation.  This method will raise an ArgumentError
      #   if multiple updates are described for a single attribute.
      #
      # @return [nil] See the documentation for the `:return` option
      #   above.
      def update(options = {})
        builder = UpdateBuilder.new
        yield(builder)
        do_updates({ :attribute_updates => builder.updates },
                   options)
      end

      # Retrieves the values of the specified attributes.
      #
      # @param [Array<String, Symbol>] attributes The names of the
      #   attributes to retrieve.  The last argument may be a hash of
      #   options for retrieving attributes from the item.  Currently
      #   the only supported option is `:consistent_read`; If set to
      #   true, then a consistent read is issued, otherwise an
      #   eventually consistent read is used.
      #
      # @return [Array] An array in which each member contains the
      #   value of the attribute at that index in the argument list.
      #   Values may be Strings, BigDecimals, Sets of Strings or Sets
      #   or BigDecimals.  If a requested attribute does not exist,
      #   the corresponding member of the output array will be `nil`.
      def values_at(*attributes)
        options = {}
        options = attributes.pop if attributes.last.kind_of?(Hash)

        return [] if attributes.empty?

        attributes.map! { |a| a.to_s }
        response_attributes =
          get_item(options, :attributes_to_get => attributes)

        values_from_response_hash(response_attributes).
          values_at(*attributes)
      end

      # @param [Hash] options Options for retrieving attributes from
      #   the item.
      #
      # @option options [Boolean] :consistent_read If set to true,
      #   then a consistent read is issued, otherwise an eventually
      #   consistent read is used.
      def to_hash options = {}
        values_from_response_hash(get_item(options))
      end
      alias_method :to_h, :to_hash

      private
      def item_key_options(options = {})
        super(item, options)
      end

      private
      def get_item(their_options, our_options = {})
        options = their_options.merge(our_options)
        resp = client.get_item(item_key_options(options))
        resp.data['Item'] || {}
      end

      private
      def do_updates(client_opts, options)
        return nil if client_opts[:attribute_updates].empty?

        client_opts[:return_values] = options[:return].to_s.upcase if
          options[:return]

        expected = expect_conditions(options)
        client_opts[:expected] = expected unless expected.empty?

        resp = client.update_item(item_key_options(client_opts))

        values_from_response_hash(resp.data["Attributes"]) if
          options[:return] and resp.data["Attributes"]
      end

    end

  end
end