aws-sdk 1.2.6 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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