aws-sdk 1.2.6 → 1.3.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 (60) hide show
  1. data/lib/aws.rb +2 -0
  2. data/lib/aws/api_config/DynamoDB-2011-12-05.yml +721 -0
  3. data/lib/aws/core.rb +10 -1
  4. data/lib/aws/core/client.rb +17 -12
  5. data/lib/aws/core/configuration.rb +13 -3
  6. data/lib/aws/core/configured_json_client_methods.rb +71 -0
  7. data/lib/aws/core/lazy_error_classes.rb +7 -2
  8. data/lib/aws/core/option_grammar.rb +67 -13
  9. data/lib/aws/core/resource.rb +9 -1
  10. data/lib/aws/core/session_signer.rb +95 -0
  11. data/lib/aws/dynamo_db.rb +169 -0
  12. data/lib/aws/dynamo_db/attribute_collection.rb +460 -0
  13. data/lib/aws/dynamo_db/batch_get.rb +206 -0
  14. data/lib/aws/dynamo_db/client.rb +119 -0
  15. data/lib/aws/dynamo_db/config.rb +20 -0
  16. data/lib/aws/dynamo_db/errors.rb +57 -0
  17. data/lib/aws/dynamo_db/expectations.rb +40 -0
  18. data/lib/aws/dynamo_db/item.rb +130 -0
  19. data/lib/aws/dynamo_db/item_collection.rb +837 -0
  20. data/lib/aws/{record/optimistic_locking.rb → dynamo_db/item_data.rb} +9 -12
  21. data/lib/aws/{record/attributes/boolean.rb → dynamo_db/keys.rb} +15 -23
  22. data/lib/aws/dynamo_db/primary_key_element.rb +47 -0
  23. data/lib/aws/dynamo_db/request.rb +78 -0
  24. data/lib/aws/{record/attributes/float.rb → dynamo_db/resource.rb} +10 -25
  25. data/lib/aws/dynamo_db/table.rb +418 -0
  26. data/lib/aws/dynamo_db/table_collection.rb +165 -0
  27. data/lib/aws/dynamo_db/types.rb +86 -0
  28. data/lib/aws/ec2/resource_tag_collection.rb +3 -1
  29. data/lib/aws/record.rb +36 -8
  30. data/lib/aws/record/abstract_base.rb +642 -0
  31. data/lib/aws/record/attributes.rb +384 -0
  32. data/lib/aws/record/dirty_tracking.rb +0 -1
  33. data/lib/aws/record/errors.rb +0 -8
  34. data/lib/aws/record/hash_model.rb +163 -0
  35. data/lib/aws/record/hash_model/attributes.rb +182 -0
  36. data/lib/aws/record/hash_model/finder_methods.rb +178 -0
  37. data/lib/aws/record/hash_model/scope.rb +108 -0
  38. data/lib/aws/record/model.rb +429 -0
  39. data/lib/aws/record/model/attributes.rb +377 -0
  40. data/lib/aws/record/model/finder_methods.rb +232 -0
  41. data/lib/aws/record/model/scope.rb +213 -0
  42. data/lib/aws/record/scope.rb +43 -169
  43. data/lib/aws/record/validations.rb +11 -11
  44. data/lib/aws/s3/client.rb +9 -6
  45. data/lib/aws/s3/object_collection.rb +1 -1
  46. data/lib/aws/simple_db/expect_condition_option.rb +1 -1
  47. data/lib/aws/simple_db/item_collection.rb +5 -3
  48. data/lib/aws/sts/client.rb +9 -0
  49. metadata +73 -30
  50. data/lib/aws/record/attribute.rb +0 -94
  51. data/lib/aws/record/attribute_macros.rb +0 -312
  52. data/lib/aws/record/attributes/date.rb +0 -89
  53. data/lib/aws/record/attributes/datetime.rb +0 -86
  54. data/lib/aws/record/attributes/integer.rb +0 -68
  55. data/lib/aws/record/attributes/sortable_float.rb +0 -60
  56. data/lib/aws/record/attributes/sortable_integer.rb +0 -95
  57. data/lib/aws/record/attributes/string.rb +0 -69
  58. data/lib/aws/record/base.rb +0 -828
  59. data/lib/aws/record/finder_methods.rb +0 -230
  60. data/lib/aws/record/scopes.rb +0 -55
@@ -0,0 +1,165 @@
1
+ # Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ module AWS
15
+ class DynamoDB
16
+
17
+ # Represents the tables in your account. Each table is
18
+ # represented by an instance of the {Table} class.
19
+ #
20
+ # == Schemas
21
+ #
22
+ # Before you can operate on items in a table you must specify the schema.
23
+ # You do this by calling #hash_key= (and optionally #range_key=) on
24
+ # a table.
25
+ #
26
+ # table = dynamo_db.tables['mytable']
27
+ # table.hash_key = [:id, :string]
28
+ #
29
+ # @example Creating a Table
30
+ # table = dynamo_db.tables.create('mytable', 10, 10, :hash_key => { :id => :string })
31
+ #
32
+ # @example Enumerating Tables
33
+ # dynamo_db.tables.each {|table| puts table.name }
34
+ #
35
+ # @example Getting a Table by Name
36
+ # table = dynamo_db.tables['mytable']
37
+ #
38
+ class TableCollection
39
+
40
+ include Core::Collection::Limitable
41
+
42
+ # Creates a new table.
43
+ #
44
+ # table = dynamo_db.tables.create('mytable', 25, 25,
45
+ # :hash_key => { :id => :string })
46
+ #
47
+ # @note Creating a table is an eventualy consistent operation. You
48
+ # can not interact with the table until its status
49
+ # ({Table#status}) is +:active+.
50
+ #
51
+ # @param [String] name The name of the table.
52
+ #
53
+ # @param [Integer] read_capacity_units Sets the minimum
54
+ # number of reads supported before read requests are throttled.
55
+ #
56
+ # @param [Integer] write_capacity_units Sets the minimum
57
+ # number of writes supported before writes requests are throttled.
58
+ #
59
+ # @param [Hash] options
60
+ #
61
+ # @option options [Hash] :hash_key A hash key is a combination
62
+ # of an attribute name and type. If you want to have the
63
+ # hash key on the string attribute username you would call #create
64
+ # with:
65
+ #
66
+ # :hash_key => { :username => :string }
67
+ #
68
+ # The other supported type is +:number+. If you wanted to
69
+ # set the hash key on a numeric (integer) attribute then you
70
+ # could call #create with:
71
+ #
72
+ # :hash_key => { :id => :number }
73
+ #
74
+ # All tables require a hash key. If +:hash_key+ is not provided
75
+ # then a default hash key will be provided. The default hash
76
+ # key is:
77
+ #
78
+ # :hash_key => { :id => :string }
79
+ #
80
+ # @option options [String] :range_key You can setup a table to use
81
+ # composite keys by providing a +:range_key+. Range keys are
82
+ # configured the same way as hash keys. They are useful
83
+ # for ordering items that share the same hash key.
84
+ #
85
+ # @return [Table] The newly created table.
86
+ #
87
+ def create name, read_capacity_units, write_capacity_units, options = {}
88
+
89
+ client_opts = {
90
+ :table_name => name.to_s,
91
+ :key_schema => key_schema(options),
92
+ :provisioned_throughput => {
93
+ :read_capacity_units => read_capacity_units,
94
+ :write_capacity_units => write_capacity_units,
95
+ },
96
+ }
97
+
98
+ response = client.create_table(client_opts)
99
+
100
+ Table.new(name, :config => config)
101
+
102
+ end
103
+
104
+ # References a table by name.
105
+ #
106
+ # dynamo_db.tables["MyTable"]
107
+ #
108
+ # @param [String] name
109
+ # @return [Table] Returns the table with the given name.
110
+ def [] name
111
+ Table.new(name, :config => config)
112
+ end
113
+
114
+ # @private
115
+ protected
116
+ def _each_item next_token, limit, options = {}, &block
117
+
118
+ options[:limit] = limit if limit
119
+ options[:exclusive_start_table_name] = next_token if next_token
120
+
121
+ response = client.list_tables(options)
122
+ response.data['TableNames'].each do |name|
123
+ yield Table.new(name, :config => config)
124
+ end
125
+
126
+ response.data['LastEvaluatedTableName']
127
+
128
+ end
129
+
130
+ private
131
+ def key_schema options
132
+
133
+ hash_key, range_key = options.values_at(:hash_key, :range_key)
134
+
135
+ # default options for :hash_key
136
+ hash_key ||= { :id => :string }
137
+
138
+ schema = {}
139
+ schema[:hash_key_element] = schema_element(hash_key, "hash")
140
+ if range_key
141
+ schema[:range_key_element] = schema_element(range_key, "range")
142
+ end
143
+ schema
144
+
145
+ end
146
+
147
+ private
148
+ def schema_element desc, key_type
149
+
150
+ (name, type) = desc.to_a.first
151
+
152
+ unless type == :string or type == :number
153
+ msg = "invalid #{key_type} key type, expected :string or :number"
154
+ raise ArgumentError, msg
155
+ end
156
+
157
+ { :attribute_name => name.to_s,
158
+ :attribute_type => type.to_s[0,1].upcase }
159
+
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,86 @@
1
+ # Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'bigdecimal'
15
+ require 'set'
16
+
17
+ module AWS
18
+ class DynamoDB
19
+
20
+ # @private
21
+ module Types
22
+
23
+ def value_from_response(hash)
24
+ (type, value) = hash.to_a.first
25
+ case type
26
+ when "S", :s
27
+ value
28
+ when "SS", :ss
29
+ Set[*value]
30
+ when "N", :n
31
+ BigDecimal(value.to_s)
32
+ when "NS", :ns
33
+ Set[*value.map { |v| BigDecimal(v.to_s) }]
34
+ end
35
+ end
36
+
37
+ def values_from_response_hash(hash)
38
+ hash.inject({}) do |h, (key, value_hash)|
39
+ h.update(key => value_from_response(value_hash))
40
+ end
41
+ end
42
+
43
+ def format_attribute_value(value, context = nil)
44
+ indicator = type_indicator(value, context)
45
+
46
+ value = [] if value == :empty_number_set
47
+ value = value.to_s if indicator == :n
48
+ value = value.map(&:to_s) if indicator == :ns
49
+
50
+ Hash[[[indicator, value]]]
51
+ end
52
+
53
+ protected
54
+ def type_indicator(value, context)
55
+ case
56
+ when value.respond_to?(:to_str) then :s
57
+ when value.kind_of?(Numeric) then :n
58
+ when value.respond_to?(:each)
59
+ indicator = nil
60
+ value.each do |v|
61
+ member_indicator = type_indicator(v, context)
62
+ raise_error("nested collections", context) if
63
+ member_indicator.to_s.size > 1
64
+ raise_error("mixed types", context) if
65
+ indicator and member_indicator != indicator
66
+ indicator = member_indicator
67
+ end
68
+ indicator ||= :s
69
+ :"#{indicator}s"
70
+ when value == :empty_number_set
71
+ :ns
72
+ else
73
+ raise_error("unsupported attribute type #{value.class}", context)
74
+ end
75
+ end
76
+
77
+ protected
78
+ def raise_error(msg, context)
79
+ msg = "#{msg} in #{context}" if context
80
+ raise ArgumentError, msg
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end
@@ -126,7 +126,9 @@ module AWS
126
126
  # tags.color # => "red"
127
127
  def method_missing(m, *args)
128
128
  if m.to_s[-1,1] == "="
129
- send(:[]=, m.to_s[0...-1], *args)
129
+ self.send(:[]=, m.to_s[0...-1], *args)
130
+ elsif args.empty?
131
+ self[m]
130
132
  else
131
133
  super
132
134
  end
@@ -14,12 +14,16 @@
14
14
  require 'set'
15
15
 
16
16
  module AWS
17
+
18
+ # AWS::Record is an ORM built on top of AWS services.
17
19
  module Record
18
20
 
19
21
  AWS.register_autoloads(self) do
20
- autoload :Base, 'base'
22
+ autoload :Base, 'model'
23
+ autoload :Model, 'model'
24
+ autoload :HashModel, 'hash_model'
21
25
  end
22
-
26
+
23
27
  # @private
24
28
  class RecordNotFound < StandardError; end
25
29
 
@@ -37,14 +41,38 @@ module AWS
37
41
  # @param [String] A prefix to append to all domains. This is useful for
38
42
  # grouping domains used by one application with a single prefix.
39
43
  def self.domain_prefix= prefix
40
- @prefix = prefix
44
+ @domain_prefix = prefix
41
45
  end
42
-
46
+
43
47
  # @return [String,nil] The string that is prepended to all domain names.
44
48
  def self.domain_prefix
45
- @prefix
49
+ @domain_prefix
46
50
  end
47
-
51
+
52
+ # Sets a prefix to be applied to all DynamoDB tables associated
53
+ # with {AWS::Record::HashModel} and {AWS::Record::ListModel}
54
+ # classes.
55
+ #
56
+ # AWS::Record.table_prefix = 'production_'
57
+ #
58
+ # class Product < AWS::Record::HashModel
59
+ # set_table_name 'products'
60
+ # end
61
+ #
62
+ # Product.table_name #=> 'production_products'
63
+ #
64
+ # @param [String] A prefix to append to all tables. This is
65
+ # useful for grouping tables used by one application with a
66
+ # single prefix.
67
+ def self.table_prefix= prefix
68
+ @table_prefix = prefix
69
+ end
70
+
71
+ # @return [String,nil] The string that is prepended to all table names.
72
+ def self.table_prefix
73
+ @table_prefix
74
+ end
75
+
48
76
  # A utility method for casting values into an array.
49
77
  #
50
78
  # * nil is returned as an empty array, []
@@ -62,7 +90,7 @@ module AWS
62
90
  else [value]
63
91
  end
64
92
  end
65
-
93
+
66
94
  # A utility method for casting values into
67
95
  #
68
96
  # * Sets are returned unmodified
@@ -77,6 +105,6 @@ module AWS
77
105
  else Set.new(as_array(value))
78
106
  end
79
107
  end
80
-
108
+
81
109
  end
82
110
  end
@@ -0,0 +1,642 @@
1
+ # Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'uuidtools'
15
+ require 'set'
16
+
17
+ require 'aws/record/scope'
18
+ require 'aws/record/naming'
19
+ require 'aws/record/validations'
20
+ require 'aws/record/dirty_tracking'
21
+ require 'aws/record/conversion'
22
+ require 'aws/record/errors'
23
+ require 'aws/record/exceptions'
24
+
25
+ module AWS
26
+ module Record
27
+ module AbstractBase
28
+
29
+ def self.extended base
30
+
31
+ base.send(:extend, ClassMethods)
32
+ base.send(:include, InstanceMethods)
33
+ base.send(:include, DirtyTracking)
34
+ base.send(:extend, Validations)
35
+
36
+ # these 3 modules are for rails 3+ active model compatability
37
+ base.send(:extend, Naming)
38
+ base.send(:include, Naming)
39
+ base.send(:include, Conversion)
40
+
41
+ end
42
+
43
+ module InstanceMethods
44
+
45
+ # Constructs a new record.
46
+ #
47
+ # @param [Hash] attributes A set of attribute values to seed this record
48
+ # with. The attributes are bulk assigned.
49
+ #
50
+ # @param [Hash] attributes Attributes that should be bulk assigned
51
+ # to this record. You can also specify the shard (i.e. domain
52
+ # or table) this record should persist to via +:shard+).
53
+ #
54
+ # @option attributes [String] :shard The domain/table this record
55
+ # should persist to. If this is omitted, it will persist to the
56
+ # class default shard (which defaults to the class name).
57
+ #
58
+ # @return [Model,HashModel] Returns a new (non-persisted) record.
59
+ # Call {#save} to persist changes to AWS.
60
+ #
61
+ def initialize attributes = {}
62
+
63
+ attributes = attributes.dup
64
+
65
+ # supporting :domain for backwards compatability, :shard is prefered
66
+ @_shard = attributes.delete(:domain)
67
+ @_shard ||= attributes.delete('domain')
68
+ @_shard ||= attributes.delete(:shard)
69
+ @_shard ||= attributes.delete('shard')
70
+ @_shard = self.class.shard_name(@_shard)
71
+
72
+ @_data = {}
73
+ assign_default_values
74
+ bulk_assign(attributes)
75
+
76
+ end
77
+
78
+ # @return [String] Returns the name of the shard this record
79
+ # is persisted to or will be persisted to. Defaults to the
80
+ # domain/table named after this record class.
81
+ def shard
82
+ @_shard
83
+ end
84
+ alias_method :domain, :shard # for backwards compatability
85
+
86
+ # The id for each record is auto-generated. The default strategy
87
+ # generates uuid strings.
88
+ # @return [String] Returns the id string (uuid) for this record. Retuns
89
+ # nil if this is a new record that has not been persisted yet.
90
+ def id
91
+ @_id
92
+ end
93
+
94
+ # @return [Hash] A hash with attribute names as hash keys (strings) and
95
+ # attribute values (of mixed types) as hash values.
96
+ def attributes
97
+ attributes = Core::IndifferentHash.new
98
+ attributes['id'] = id if persisted?
99
+ self.class.attributes.keys.inject(attributes) do |hash,attr_name|
100
+ hash.merge(attr_name => __send__(attr_name))
101
+ end
102
+ end
103
+
104
+ # Acts like {#update} but does not call {#save}.
105
+ #
106
+ # record.attributes = { :name => 'abc', :age => 20 }
107
+ #
108
+ # @param [Hash] attributes A hash of attributes to set on this record
109
+ # without calling save.
110
+ #
111
+ # @return [Hash] Returns the attribute hash that was passed in.
112
+ #
113
+ def attributes= attributes
114
+ bulk_assign(attributes)
115
+ end
116
+
117
+ # Persistence indicates if the record has been saved previously or not.
118
+ #
119
+ # @example
120
+ # @recipe = Recipe.new(:name => 'Buttermilk Pancackes')
121
+ # @recipe.persisted? #=> false
122
+ # @recipe.save!
123
+ # @recipe.persisted? #=> true
124
+ #
125
+ # @return [Boolean] Returns true if this record has been persisted.
126
+ def persisted?
127
+ !!@_persisted
128
+ end
129
+
130
+ # @return [Boolean] Returns true if this record has not been persisted
131
+ # to SimpleDB.
132
+ def new_record?
133
+ !persisted?
134
+ end
135
+
136
+ # @return [Boolean] Returns true if this record has no validation errors.
137
+ def valid?
138
+ run_validations
139
+ errors.empty?
140
+ end
141
+
142
+ def errors
143
+ @errors ||= Errors.new
144
+ end
145
+
146
+ # Creates new records, updates existing records.
147
+ # @return [Boolean] Returns true if the record saved without errors,
148
+ # false otherwise.
149
+ def save
150
+ if valid?
151
+ persisted? ? update : create
152
+ clear_changes!
153
+ true
154
+ else
155
+ false
156
+ end
157
+ end
158
+
159
+ # Creates new records, updates exsting records. If there is a validation
160
+ # error then an exception is raised.
161
+ # @raise [InvalidRecordError] Raised when the record has validation
162
+ # errors and can not be saved.
163
+ # @return [true] Returns true after a successful save.
164
+ def save!
165
+ raise InvalidRecordError.new(self) unless save
166
+ true
167
+ end
168
+
169
+ # Bulk assigns the attributes and then saves the record.
170
+ # @param [Hash] attribute_hash A hash of attribute names (keys) and
171
+ # attribute values to assign to this record.
172
+ # @return (see #save)
173
+ def update_attributes attribute_hash
174
+ bulk_assign(attribute_hash)
175
+ save
176
+ end
177
+
178
+ # Bulk assigns the attributes and then saves the record. Raises
179
+ # an exception (AWS::Record::InvalidRecordError) if the record is not
180
+ # valid.
181
+ # @param (see #update_attributes)
182
+ # @return [true]
183
+ def update_attributes! attribute_hash
184
+ if update_attributes(attribute_hash)
185
+ true
186
+ else
187
+ raise InvalidRecordError.new(self)
188
+ end
189
+ end
190
+
191
+ # Deletes the record.
192
+ # @return [true]
193
+ def delete
194
+ if persisted?
195
+ if deleted?
196
+ raise 'unable to delete, this object has already been deleted'
197
+ else
198
+ delete_storage
199
+ @_deleted = true
200
+ end
201
+ else
202
+ raise 'unable to delete, this object has not been saved yet'
203
+ end
204
+ end
205
+
206
+ # @return [Boolean] Returns true if this instance object has been deleted.
207
+ def deleted?
208
+ persisted? ? !!@_deleted : false
209
+ end
210
+
211
+ # If you define a custom setter, you use #[]= to set the value
212
+ # on the record.
213
+ #
214
+ # class Book < AWS::Record::Model
215
+ #
216
+ # string_attr :name
217
+ #
218
+ # # replace the default #author= method
219
+ # def author= name
220
+ # self['author'] = name.blank? ? 'Anonymous' : name
221
+ # end
222
+ #
223
+ # end
224
+ #
225
+ # @param [String,Symbol] The attribute name to set a value for
226
+ # @param attribute_value The value to assign.
227
+ protected
228
+ def []= attribute_name, new_value
229
+ self.class.attribute_for(attribute_name) do |attribute|
230
+
231
+ if_tracking_changes do
232
+ original_value = type_cast(attribute, attribute_was(attribute.name))
233
+ incoming_value = type_cast(attribute, new_value)
234
+ if original_value == incoming_value
235
+ clear_change!(attribute.name)
236
+ else
237
+ attribute_will_change!(attribute.name)
238
+ end
239
+ end
240
+
241
+ @_data[attribute.name] = new_value
242
+
243
+ end
244
+ end
245
+
246
+ # Returns the typecasted value for the named attribute.
247
+ #
248
+ # book = Book.new(:title => 'My Book')
249
+ # book['title'] #=> 'My Book'
250
+ # book.title #=> 'My Book'
251
+ #
252
+ # === Intended Use
253
+ #
254
+ # This method's primary use is for getting/setting the value for
255
+ # an attribute inside a custom method:
256
+ #
257
+ # class Book < AWS::Record::Model
258
+ #
259
+ # string_attr :title
260
+ #
261
+ # def title
262
+ # self['title'] ? self['title'].upcase : nil
263
+ # end
264
+ #
265
+ # end
266
+ #
267
+ # book = Book.new(:title => 'My Book')
268
+ # book.title #=> 'MY BOOK'
269
+ #
270
+ # @param [String,Symbol] attribute_name The name of the attribute to fetch
271
+ # a value for.
272
+ # @return The current type-casted value for the named attribute.
273
+ protected
274
+ def [] attribute_name
275
+ self.class.attribute_for(attribute_name) do |attribute|
276
+ type_cast(attribute, @_data[attribute.name])
277
+ end
278
+ end
279
+
280
+ protected
281
+ def create
282
+ populate_id
283
+ touch_timestamps('created_at', 'updated_at')
284
+ increment_optimistic_lock_value
285
+ create_storage
286
+ @_persisted = true
287
+ end
288
+
289
+ private
290
+ def update
291
+ return unless changed?
292
+ touch_timestamps('updated_at')
293
+ increment_optimistic_lock_value
294
+ update_storage
295
+ end
296
+
297
+ protected
298
+ def populate_id
299
+ @_id = UUIDTools::UUID.random_create.to_s
300
+ end
301
+
302
+ protected
303
+ def touch_timestamps *attributes
304
+ now = Time.now
305
+ attributes.each do |attr_name|
306
+ if
307
+ self.class.attributes[attr_name] and
308
+ !attribute_changed?(attr_name)
309
+ # don't touch timestamps the user modified
310
+ then
311
+ __send__("#{attr_name}=", now)
312
+ end
313
+ end
314
+ end
315
+
316
+ protected
317
+ def increment_optimistic_lock_value
318
+ if_locks_optimistically do |lock_attr|
319
+ if value = self[lock_attr.name]
320
+ self[lock_attr.name] = value + 1
321
+ else
322
+ self[lock_attr.name] = 1
323
+ end
324
+ end
325
+ end
326
+
327
+ protected
328
+ def if_locks_optimistically &block
329
+ if opt_lock_attr = self.class.optimistic_locking_attr
330
+ yield(opt_lock_attr)
331
+ end
332
+ end
333
+
334
+ protected
335
+ def opt_lock_conditions
336
+ conditions = {}
337
+ if_locks_optimistically do |lock_attr|
338
+ if was = attribute_was(lock_attr.name)
339
+ conditions[:if] = { lock_attr.name => lock_attr.serialize(was) }
340
+ else
341
+ conditions[:unless_exists] = lock_attr.name
342
+ end
343
+ end
344
+ conditions
345
+ end
346
+
347
+ private
348
+ def assign_default_values
349
+ # populate default attribute values
350
+ ignore_changes do
351
+ self.class.attributes.values.each do |attribute|
352
+ begin
353
+ # copy default values down so methods like #gsub! don't
354
+ # modify the default values for other objects
355
+ @_data[attribute.name] = attribute.default_value.clone
356
+ rescue TypeError
357
+ @_data[attribute.name] = attribute.default_value
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ private
364
+ def bulk_assign hash
365
+ flatten_date_parts(hash).each_pair do |attr_name, attr_value|
366
+ __send__("#{attr_name}=", attr_value)
367
+ end
368
+ end
369
+
370
+ private
371
+ # Rails date and time select helpers split date and time
372
+ # attributes into multiple values for form submission.
373
+ # These attributes get named things like 'created_at(1i)'
374
+ # and represent year/month/day/hour/min/sec parts of
375
+ # the date/time.
376
+ #
377
+ # This method converts these attributes back into a single
378
+ # value and converts them to Date and DateTime objects.
379
+ def flatten_date_parts attributes
380
+
381
+ multi_attributes = Set.new
382
+
383
+ hash = attributes.inject({}) do |hash,(key,value)|
384
+ # collects attribuets like "created_at(1i)" into an array of parts
385
+ if key =~ /\(/
386
+ key, index = key.to_s.split(/\(|i\)/)
387
+ hash[key] ||= []
388
+ hash[key][index.to_i - 1] = value.to_i
389
+ multi_attributes << key
390
+ else
391
+ hash[key] = value
392
+ end
393
+ hash
394
+ end
395
+
396
+ # convert multiattribute values into date/time objects
397
+ multi_attributes.each do |key|
398
+
399
+ values = hash[key]
400
+
401
+ hash[key] = case values.size
402
+ when 0 then nil
403
+ when 2
404
+ now = Time.now
405
+ Time.local(now.year, now.month, now.day, values[0], values[1], 0, 0)
406
+ when 3 then Date.new(*values)
407
+ else DateTime.new(*values)
408
+ end
409
+
410
+ end
411
+
412
+ hash
413
+
414
+ end
415
+
416
+ private
417
+ def type_cast attribute, raw
418
+ if attribute.set?
419
+ values = Record.as_array(raw).inject([]) do |values,value|
420
+ values << attribute.type_cast(value)
421
+ values
422
+ end
423
+ Set.new(values.compact)
424
+ else
425
+ attribute.type_cast(raw)
426
+ end
427
+ end
428
+
429
+ private
430
+ def serialize_attributes
431
+
432
+ hash = {}
433
+ self.class.attributes.each_pair do |attribute_name,attribute|
434
+ value = serialize_attribute(attribute, @_data[attribute_name])
435
+ unless [nil, []].include?(value)
436
+ hash[attribute_name] = value
437
+ end
438
+ end
439
+
440
+ # simple db does not support persisting items without attribute values
441
+ raise EmptyRecordError.new(self) if hash.empty?
442
+
443
+ hash
444
+
445
+ end
446
+
447
+ private
448
+ def serialize_attribute attribute, raw_value
449
+ type_casted_value = type_cast(attribute, raw_value)
450
+ case type_casted_value
451
+ when nil then nil
452
+ when Set then type_casted_value.map{|v| attribute.serialize(v) }
453
+ else attribute.serialize(type_casted_value)
454
+ end
455
+ end
456
+
457
+ # @private
458
+ protected
459
+ def hydrate id, data
460
+
461
+ # @todo need to do something about partial hyrdation of attributes
462
+
463
+ @_id = id
464
+
465
+ # New objects are populated with default values, but we don't
466
+ # want these values to hang around when hydrating persisted values
467
+ # (those values may have been blanked out before save).
468
+ self.class.attributes.values.each do |attribute|
469
+ @_data[attribute.name] = nil
470
+ end
471
+
472
+ ignore_changes do
473
+ bulk_assign(deserialize_item_data(data))
474
+ end
475
+
476
+ @_persisted = true
477
+
478
+ end
479
+
480
+ protected
481
+ def create_storage
482
+ raise NotImplementedError
483
+ end
484
+
485
+ protected
486
+ def update_storage
487
+ raise NotImplementedError
488
+ end
489
+
490
+ protected
491
+ def delete_storage
492
+ raise NotImplementedError
493
+ end
494
+
495
+ end
496
+
497
+ module ClassMethods
498
+
499
+ # Allows you to override the default shard name for this class.
500
+ # The shard name defaults to the class name.
501
+ # @param [String] name
502
+ def set_shard_name name
503
+ @_shard_name = name
504
+ end
505
+ alias_method :set_domain_name, :set_shard_name
506
+ alias_method :shard_name=, :set_shard_name
507
+
508
+ # Returns the name of the shard this class will persist records
509
+ # into by default.
510
+ #
511
+ # @param [String] name Defaults to the name of this class.
512
+ # @return [String] Returns the full prefixed domain name for this class.
513
+ def shard_name name = nil
514
+ name = @_shard_name if name.nil?
515
+ name = self.name if name.nil?
516
+ name = name.name if name.is_a?(Core::Model) # sdb domain or ddb table
517
+ name
518
+ end
519
+ alias_method :domain_name, :shard_name
520
+
521
+ # Adds a scoped finder to this class.
522
+ #
523
+ # class Book < AWS::Record::Model
524
+ # scope :top_10, order(:popularity, :desc).limit(10)
525
+ # end
526
+ #
527
+ # Book.top_10.to_a
528
+ # #=> [#<Book...>, #<Book...>]
529
+ #
530
+ # Book.top_10.first
531
+ # #=> #<Book...>
532
+ #
533
+ # You can also provide a block that accepts params for the scoped
534
+ # finder. This block should return a scope.
535
+ #
536
+ # class Book < AWS::Record::Model
537
+ # scope :by_author, lambda {|name| where(:author => name) }
538
+ # end
539
+ #
540
+ # # top 10 books by the author 'John Doe'
541
+ # Book.by_author('John Doe').top_10
542
+ #
543
+ # @param [Symbol] name The name of the scope. Scope names should be
544
+ # method-safe and should not conflict with any other class methods.
545
+ #
546
+ # @param [Scope] scope
547
+ #
548
+ def scope name, scope = nil, &block
549
+
550
+ method_definition = scope ? lambda { scope } : block
551
+
552
+ extend(Module.new { define_method(name, &method_definition) })
553
+
554
+ end
555
+
556
+ # @private
557
+ def new_scope
558
+ self::Scope.new(self)
559
+ end
560
+
561
+ def optimistic_locking attribute_name = :version_id
562
+ attribute = integer_attr(attribute_name)
563
+ @optimistic_locking_attr = attribute
564
+ end
565
+
566
+ # @return [Boolean] Returns true if this class is configured to
567
+ # perform optimistic locking.
568
+ def optimistic_locking?
569
+ !!@optimistic_locking_attr
570
+ end
571
+
572
+ @private
573
+ def optimistic_locking_attr
574
+ @optimistic_locking_attr
575
+ end
576
+
577
+ # @return [Hash<String,Attribute>] Returns a hash of all of the
578
+ # configured attributes for this class.
579
+ def attributes
580
+ @attributes ||= {}
581
+ end
582
+
583
+ # @private
584
+ def attribute_for attribute_name, &block
585
+ unless attribute = attributes[attribute_name.to_s]
586
+ raise UndefinedAttributeError.new(attribute_name.to_s)
587
+ end
588
+ block_given? ? yield(attribute) : attribute
589
+ end
590
+
591
+ # @private
592
+ def add_attribute attribute
593
+
594
+ attr_name = attribute.name
595
+
596
+ attributes[attr_name] = attribute
597
+
598
+ # setter
599
+ define_method("#{attr_name}=") do |value|
600
+ self[attr_name] = value
601
+ end
602
+
603
+ # getter
604
+ define_method(attr_name) do
605
+ self[attr_name]
606
+ end
607
+
608
+ # before type-cast getter
609
+ define_method("#{attr_name}_before_type_cast") do
610
+ @_data[attr_name]
611
+ end
612
+
613
+ ## dirty tracking methods
614
+
615
+ define_method("#{attr_name}_changed?") do
616
+ attribute_changed?(attr_name)
617
+ end
618
+
619
+ define_method("#{attr_name}_change") do
620
+ attribute_change(attr_name)
621
+ end
622
+
623
+ define_method("#{attr_name}_was") do
624
+ attribute_was(attr_name)
625
+ end
626
+
627
+ define_method("#{attr_name}_will_change!") do
628
+ attribute_will_change!(attr_name)
629
+ end
630
+
631
+ define_method("reset_#{attr_name}!") do
632
+ reset_attribute!(attr_name)
633
+ end
634
+
635
+ attribute
636
+
637
+ end
638
+
639
+ end
640
+ end
641
+ end
642
+ end