File: model.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 (453 lines) | stat: -rw-r--r-- 13,513 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
# 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.

# todo move these to included modules (like validations and naming)


module AWS
  module Record

    # An ActiveRecord-like interface built ontop of Amazon SimpleDB.
    #
    #     class Book < AWS::Record::Model
    #
    #       string_attr :title
    #       string_attr :author
    #       integer_attr :number_of_pages
    #
    #       timestamps # adds a :created_at and :updated_at pair of timestamps
    #
    #     end
    #
    #     b = Book.new(:title => 'My Book', :author => 'Me', :pages => 1)
    #     b.save
    #
    # # Attribute Macros
    #
    # When extending AWS::Record::Model you should first consider what
    # attributes your class should have.  Unlike ActiveRecord, AWS::Record
    # models are not backed by a database table/schema.  You must choose what
    # attributes (and what types) you need.
    #
    # * `string_attr`
    # * `boolean_attr`
    # * `integer_attr`
    # * `float_attr`
    # * `datetime_attr`
    # * `date_attr`
    #
    # ### Usage
    #
    # Normally you just call these methods inside your model class definition:
    #
    #     class Book < AWS::Record::Model
    #       string_attr :title
    #       boolean_attr :has_been_read
    #       integer_attr :number_of_pages
    #       float_attr :weight_in_pounds
    #       datetime_attr :published_at
    #     end
    #
    # For each attribute macro a pair of setter/getter methods are added #
    # to your class (and a few other useful methods).
    #
    #     b = Book.new
    #     b.title = "My Book"
    #     b.has_been_read = true
    #     b.number_of_pages = 1000
    #     b.weight_in_pounds = 1.1
    #     b.published_at = Time.now
    #     b.save
    #
    #     b.id #=> "0aa894ca-8223-4d34-831e-e5134b2bb71c"
    #     b.attributes
    #     #=> { 'title' => 'My Book', 'has_been_read' => true, ... }
    #
    # ### Default Values
    #
    # All attribute macros accept the `:default_value` option.  This sets
    # a value that is populated onto all new instnaces of the class.
    #
    #     class Book < AWS::Record::Model
    #       string_attr :author, :default_value => 'Me'
    #     end
    #
    #     Book.new.author #=> 'Me'
    #
    # ### Multi-Valued (Set) Attributes
    #
    # AWS::Record permits storing multiple values with a single attribute.
    #
    #     class Book < AWS::Record::Model
    #       string_attr :tags, :set => true
    #     end
    #
    #     b = Book.new
    #     b.tags #=> #<Set: {}>
    #
    #     b.tags = ['fiction', 'fantasy']
    #     b.tags #=> #<Set: {'fiction', 'fantasy'}>
    #
    # These multi-valued attributes are treated as sets, not arrays.  This
    # means:
    #
    # * values are unordered
    # * duplicate values are automatically omitted
    #
    # Please consider these limitations when you choose to use the `:set`
    # option with the attribute macros.
    #
    # # Validations
    #
    # It's important to validate models before there are persisted to keep
    # your data clean.  AWS::Record supports most of the ActiveRecord style
    # validators.
    #
    #     class Book < AWS::Record::Model
    #       string_attr :title
    #       validates_presence_of :title
    #     end
    #
    #     b = Book.new
    #     b.valid? #=> false
    #     b.errors.full_messages #=> ['Title may not be blank']
    #
    # Validations are checked before saving a record.  If any of the validators
    # adds an error, the the save will fail.
    #
    # For more information about the available validation methods see
    # {Validations}.
    #
    # # Finder Methods
    #
    # You can find records by their ID.  Each record gets a UUID when it
    # is saved for the first time.  You can use this ID to fetch the record
    # at a latter time:
    #
    #     b = Book["0aa894ca-8223-4d34-831e-e5134b2bb71c"]
    #
    #     b = Book.find("0aa894ca-8223-4d34-831e-e5134b2bb71c")
    #
    # If you try to find a record by ID that has no data an error will
    # be raised.
    #
    # ### All
    #
    # You can enumerate all of your records using `all`.
    #
    #     Book.all.each do |book|
    #       puts book.id
    #     end
    #
    #     Book.find(:all) do |book|
    #       puts book.id
    #     end
    #
    # Be careful when enumerating all.  Depending on the number of records
    # and number of attributes each record has, this can take a while,
    # causing quite a few requests.
    #
    # ### First
    #
    # If you only want a single record, you should use `first`.
    #
    #     b = Book.first
    #
    # ### Modifiers
    #
    # Frequently you do not want ALL records or the very first record.  You
    # can pass options to `find`, `all` and `first`.
    #
    #     my_books = Book.find(:all, :where => 'owner = "Me"')
    #
    #     book = Book.first(:where => { :has_been_read => false })
    #
    # You can pass as find options:
    #
    # * `:where` - Conditions that must be met to be returned
    # * `:order` - The order to sort matched records by
    # * `:limit` - The maximum number of records to return
    #
    # # Scopes
    #
    # More useful than writing query fragments all over the place is to
    # name your most common conditions for reuse.
    #
    #     class Book < AWS::Record::Model
    #
    #       scope :mine, where(:owner => 'Me')
    #
    #       scope :unread, where(:has_been_read => false)
    #
    #       scope :by_popularity, order(:score, :desc)
    #
    #       scope :top_10, by_popularity.limit(10)
    #
    #     end
    #
    #     # The following expression returns 10 books that belong
    #     # to me, that are unread sorted by popularity.
    #     next_good_reads = Book.mine.unread.top_10
    #
    # There are 3 standard scope methods:
    #
    # * `where`
    # * `order`
    # * `limit`
    #
    # ### Conditions (where)
    #
    # Where accepts aruments in a number of forms:
    #
    # 1. As an sql-like fragment. If you need to escape values this form is
    #    not suggested.
    #
    #        Book.where('title = "My Book"')
    #
    # 2. An sql-like fragment, with placeholders.  This escapes quoted
    #    arguments properly to avoid injection.
    #
    #        Book.where('title = ?', 'My Book')
    #
    # 3. A hash of key-value pairs. This is the simplest form, but also the
    #    least flexible.  You can not use this form if you need more complex
    #    expressions that use or.
    #
    #        Book.where(:title => 'My Book')
    #
    # ### Order
    #
    # This orders the records as returned by AWS.  Default ordering is ascending.
    # Pass the value :desc as a second argument to sort in reverse ordering.
    #
    #     Book.order(:title)        # alphabetical ordering
    #     Book.order(:title, :desc) # reverse alphabetical ordering
    #
    # You may only order by a single attribute. If you call order twice in the
    # chain, the last call gets presedence:
    #
    #     Book.order(:title).order(:price)
    #
    # In this example the books will be ordered by :price and the order(:title)
    # is lost.
    #
    # ### Limit
    #
    # Just call `limit` with an integer argument.  This sets the maximum
    # number of records to retrieve:
    #
    #     Book.limit(2)
    #
    # ### Delayed Execution
    #
    # It should be noted that all finds are lazy (except `first`).  This
    # means the value returned is not an array of records, rather a handle
    # to a {Scope} object that will return records when you enumerate over them.
    #
    # This allows you to build an expression without making unecessary requests.
    # In the following example no request is made until the call to
    # each_with_index.
    #
    #     all_books = Books.all
    #     ten_books = all_books.limit(10)
    #
    #     ten_books.each_with_index do |book,n|
    #       puts "#{n + 1} : #{book.title}"
    #     end
    #
    class Model

      require 'aws/record/model/attributes'
      require 'aws/record/model/finder_methods'
      require 'aws/record/model/scope'

      extend AbstractBase

      # The id for each record is auto-generated.  The default strategy
      # generates uuid strings.
      # @return [String] Returns the id string (uuid) for this record.  Retuns
      #   nil if this is a new record that has not been persisted yet.
      def id
        @_id
      end

      # @return [Hash] A hash with attribute names as hash keys (strings) and
      #   attribute values (of mixed types) as hash values.
      def attributes
        attributes = super
        attributes['id'] = id if persisted?
        attributes
      end

      class << self

        # Creates the SimpleDB domain that is configured for this class.
        #
        #     class Product < AWS::Record::Model
        #     end
        #
        #     Product.create_table #=> 'Product'
        #
        # If you share a single AWS account with multiple applications, you
        # can provide a domain prefix for your model classes.
        #
        #     AWS::Record.domain_prefix = 'myapp-'
        #
        #     Product.create_table #=> 'myapp-Product'
        #
        # If you have set a model shard name, this is used in place of the
        # class name.
        #
        #     AWS::Record.domain_prefix = 'prod-'
        #     class Product < AWS::Record::Model
        #       set_shard_name 'products'
        #     end
        #
        #     Product.create_table #=> 'prod-products'
        #
        # If you shard you data across multiple domains, you can specify the
        # shard name:
        #
        #     # create two domains, with the given names
        #     Product.create_domain 'products-1'
        #     Product.create_domain 'products-2'
        #
        # @param [optional,String] shard_name Defaults to the class name.
        #
        # @return [SimpleDB::Domain]
        #
        def create_domain shard_name = nil
          sdb.domains.create(sdb_domain_name(shard_name))
        end

        # @return [AWS::SimpleDB::Domain]
        # @api private
        def sdb_domain shard_name = nil
          sdb.domains[sdb_domain_name(shard_name)]
        end

        protected
        def sdb_domain_name shard_name = nil
          "#{AWS::Record.domain_prefix}#{self.shard_name(shard_name)}"
        end

        protected
        def sdb
          AWS::SimpleDB.new
        end

      end

      # @return [SimpleDB::Item] Returns a reference to the item as stored in
      #   simple db.
      # @api private
      private
      def sdb_item
        sdb_domain.items[id]
      end

      # @return [SimpleDB::Domain] Returns the domain this record is
      #   persisted to or will be persisted to.
      private
      def sdb_domain
        self.class.sdb_domain(shard)
      end

      # This function accepts a hash of item data (as returned from
      # AttributeCollection#to_h or ItemData#attributes) and returns only
      # the key/value pairs that are configured attribues for this class.
      # @api private
      private
      def deserialize_item_data item_data

        marked_for_deletion = item_data['_delete_'] || []

        data = {}
        item_data.each_pair do |attr_name,values|

          attribute = self.class.attributes[attr_name]

          next unless attribute
          next if marked_for_deletion.include?(attr_name)

          if attribute.set?
            data[attr_name] = values.map{|v| attribute.deserialize(v) }
          else
            data[attr_name] = attribute.deserialize(values.first)
          end

        end
        data
      end

      def hydrate(id, data)
        @_id = id
        super
      end

      # @api private
      def populate_id
        @_id = SecureRandom.uuid
      end

      # @api private
      protected
      def create_storage
        to_add = serialize_attributes
        sdb_item.attributes.add(to_add.merge(opt_lock_conditions))
      end

      # @api private
      private
      def update_storage

        to_update = {}
        to_delete = []

        # serialized_attributes will raise error if the entire record is blank
        attribute_values = serialize_attributes

        changed.each do |attr_name|
          if values = attribute_values[attr_name]
            to_update[attr_name] = values
          else
            to_delete << attr_name
          end
        end

        to_update.merge!(opt_lock_conditions)

        if to_delete.empty?
          sdb_item.attributes.replace(to_update)
        else
          sdb_item.attributes.replace(to_update.merge('_delete_' => to_delete))
          sdb_item.attributes.delete(to_delete + ['_delete_'])
        end

      end

      # @return [true]
      # @api private
      private
      def delete_storage
        sdb_item.delete(opt_lock_conditions)
        @_deleted = true
      end

    end

    # for backwards compatability with the old AWS::Record::Base
    Base = Model

  end
end