dynamoid 3.4.0 → 3.7.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -3
  3. data/README.md +52 -26
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +15 -6
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +48 -36
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +13 -1
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +9 -8
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +6 -3
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
  17. data/lib/dynamoid/application_time_zone.rb +1 -0
  18. data/lib/dynamoid/associations.rb +182 -19
  19. data/lib/dynamoid/associations/association.rb +10 -2
  20. data/lib/dynamoid/associations/belongs_to.rb +2 -1
  21. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
  22. data/lib/dynamoid/associations/has_many.rb +2 -1
  23. data/lib/dynamoid/associations/has_one.rb +2 -1
  24. data/lib/dynamoid/associations/many_association.rb +68 -23
  25. data/lib/dynamoid/associations/single_association.rb +31 -4
  26. data/lib/dynamoid/components.rb +1 -0
  27. data/lib/dynamoid/config.rb +5 -5
  28. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
  29. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
  30. data/lib/dynamoid/config/options.rb +1 -0
  31. data/lib/dynamoid/criteria.rb +9 -1
  32. data/lib/dynamoid/criteria/chain.rb +422 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +32 -11
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
  37. data/lib/dynamoid/dirty.rb +119 -64
  38. data/lib/dynamoid/document.rb +125 -43
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/errors.rb +2 -0
  42. data/lib/dynamoid/fields.rb +217 -36
  43. data/lib/dynamoid/fields/declare.rb +86 -0
  44. data/lib/dynamoid/finders.rb +69 -32
  45. data/lib/dynamoid/identity_map.rb +6 -0
  46. data/lib/dynamoid/indexes.rb +86 -17
  47. data/lib/dynamoid/loadable.rb +2 -2
  48. data/lib/dynamoid/log/formatter.rb +26 -0
  49. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  50. data/lib/dynamoid/persistence.rb +496 -104
  51. data/lib/dynamoid/persistence/import.rb +1 -0
  52. data/lib/dynamoid/persistence/save.rb +1 -0
  53. data/lib/dynamoid/persistence/update_fields.rb +5 -2
  54. data/lib/dynamoid/persistence/update_validations.rb +18 -0
  55. data/lib/dynamoid/persistence/upsert.rb +5 -3
  56. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  57. data/lib/dynamoid/railtie.rb +1 -0
  58. data/lib/dynamoid/tasks.rb +3 -1
  59. data/lib/dynamoid/tasks/database.rb +1 -0
  60. data/lib/dynamoid/type_casting.rb +12 -2
  61. data/lib/dynamoid/undumping.rb +8 -0
  62. data/lib/dynamoid/validations.rb +6 -1
  63. data/lib/dynamoid/version.rb +1 -1
  64. metadata +48 -74
  65. data/.coveralls.yml +0 -1
  66. data/.document +0 -5
  67. data/.gitignore +0 -74
  68. data/.rspec +0 -2
  69. data/.rubocop.yml +0 -71
  70. data/.rubocop_todo.yml +0 -55
  71. data/.travis.yml +0 -44
  72. data/Appraisals +0 -22
  73. data/Gemfile +0 -8
  74. data/Rakefile +0 -46
  75. data/Vagrantfile +0 -29
  76. data/docker-compose.yml +0 -7
  77. data/dynamoid.gemspec +0 -57
  78. data/gemfiles/rails_4_2.gemfile +0 -9
  79. data/gemfiles/rails_5_0.gemfile +0 -8
  80. data/gemfiles/rails_5_1.gemfile +0 -8
  81. data/gemfiles/rails_5_2.gemfile +0 -8
  82. data/gemfiles/rails_6_0.gemfile +0 -8
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # This is the base module for all domain objects that need to be persisted to
5
5
  # the database as documents.
6
6
  module Document
@@ -17,28 +17,19 @@ module Dynamoid #:nodoc:
17
17
  end
18
18
 
19
19
  module ClassMethods
20
- # Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
21
- # write capacity.
22
- #
23
- # @param [Hash] options options to pass for this table
24
- # @option options [Symbol] :name the name for the table; this still gets namespaced
25
- # @option options [Symbol] :id id column for the table
26
- # @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
27
- # @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
28
- #
29
- # @since 0.4.0
20
+ # @private
30
21
  def table(options = {})
31
22
  self.options = options
32
23
  super if defined? super
33
24
  end
34
25
 
35
26
  def attr_readonly(*read_only_attributes)
36
- ActiveSupport::Deprecation.warn('[Dynamoid] .attr_readonly is deprecated! Call .find instead of')
37
27
  self.read_only_attributes.concat read_only_attributes.map(&:to_s)
38
28
  end
39
29
 
40
- # Returns the read_capacity for this table.
30
+ # Returns the read capacity for this table.
41
31
  #
32
+ # @return [Integer] read capacity units
42
33
  # @since 0.4.0
43
34
  def read_capacity
44
35
  options[:read_capacity] || Dynamoid::Config.read_capacity
@@ -46,32 +37,53 @@ module Dynamoid #:nodoc:
46
37
 
47
38
  # Returns the write_capacity for this table.
48
39
  #
40
+ # @return [Integer] write capacity units
49
41
  # @since 0.4.0
50
42
  def write_capacity
51
43
  options[:write_capacity] || Dynamoid::Config.write_capacity
52
44
  end
53
45
 
54
-
55
46
  # Returns the billing (capacity) mode for this table.
56
- # Could be either :provisioned or :on_demand
47
+ #
48
+ # Could be either +provisioned+ or +on_demand+.
49
+ #
50
+ # @return [Symbol]
57
51
  def capacity_mode
58
52
  options[:capacity_mode] || Dynamoid::Config.capacity_mode
59
53
  end
60
54
 
61
55
  # Returns the field name used to support STI for this table.
56
+ #
57
+ # Default field name is +type+ but it can be overrided in the +table+
58
+ # method call.
59
+ #
60
+ # User.inheritance_field # => :type
62
61
  def inheritance_field
63
62
  options[:inheritance_field] || :type
64
63
  end
65
64
 
66
- # Returns the id field for this class.
65
+ # Returns the hash key field name for this class.
66
+ #
67
+ # By default +id+ field is used. But it can be overriden in the +table+
68
+ # method call.
67
69
  #
70
+ # User.hash_key # => :id
71
+ #
72
+ # @return [Symbol] a hash key name
68
73
  # @since 0.4.0
69
74
  def hash_key
70
75
  options[:key] || :id
71
76
  end
72
77
 
73
- # Returns the number of items for this class.
78
+ # Return the count of items for this class.
79
+ #
80
+ # It returns aproximate value based on DynamoDB statistic. DynamoDB
81
+ # updates it periodicaly so the value can be no accurate.
74
82
  #
83
+ # It's a reletivly cheap operation and doesn't read all the items in a
84
+ # table. It makes just one HTTP request to DynamoDB.
85
+ #
86
+ # @return [Integer] items count in a table
75
87
  # @since 0.6.1
76
88
  def count
77
89
  Dynamoid.adapter.count(table_name)
@@ -79,35 +91,67 @@ module Dynamoid #:nodoc:
79
91
 
80
92
  # Initialize a new object.
81
93
  #
82
- # @param [Hash] attrs Attributes with which to create the object.
94
+ # User.build(name: 'A')
83
95
  #
84
- # @return [Dynamoid::Document] the new document
96
+ # Initialize an object and pass it into a block to set other attributes.
97
+ #
98
+ # User.build(name: 'A') do |u|
99
+ # u.age = 21
100
+ # end
101
+ #
102
+ # The only difference between +build+ and +new+ methods is that +build+
103
+ # supports STI (Single table inheritance) and looks at the inheritance
104
+ # field. So it can build a model of actual class. For instance:
105
+ #
106
+ # class Employee
107
+ # include Dynamoid::Document
108
+ #
109
+ # field :type
110
+ # field :name
111
+ # end
85
112
  #
113
+ # class Manager < Employee
114
+ # end
115
+ #
116
+ # Employee.build(name: 'Alice', type: 'Manager') # => #<Manager:0x00007f945756e3f0 ...>
117
+ #
118
+ # @param attrs [Hash] Attributes with which to create the document
119
+ # @param block [Proc] Block to process a document after initialization
120
+ # @return [Dynamoid::Document] the new document
86
121
  # @since 0.2.0
87
- def build(attrs = {})
88
- choose_right_class(attrs).new(attrs)
122
+ def build(attrs = {}, &block)
123
+ choose_right_class(attrs).new(attrs, &block)
89
124
  end
90
125
 
91
- # Does this object exist?
126
+ # Does this model exist in a table?
127
+ #
128
+ # User.exists?('713') # => true
92
129
  #
93
- # Supports primary key in format that `find` call understands.
94
- # Multiple keys and single compound primary key should be passed only as Array explicitily.
130
+ # If a range key is declared it should be specified in the following way:
95
131
  #
96
- # Supports conditions in format that `where` call understands.
132
+ # User.exists?([['713', 'range-key-value']]) # => true
97
133
  #
98
- # @param [Mixed] id_or_conditions the id of the object or a hash with the options to filter from.
134
+ # It's possible to check existence of several models at once:
99
135
  #
100
- # @return [Boolean] true/false
136
+ # User.exists?(['713', '714', '715'])
101
137
  #
102
- # @example With id
138
+ # Or in case when a range key is declared:
103
139
  #
104
- # Post.exist?(713)
105
- # Post.exist?([713, 210])
140
+ # User.exists?(
141
+ # [
142
+ # ['713', 'range-key-value-1'],
143
+ # ['714', 'range-key-value-2'],
144
+ # ['715', 'range-key-value-3']
145
+ # ]
146
+ # )
106
147
  #
107
- # @example With attributes conditions
148
+ # It's also possible to specify models not with primary key but with
149
+ # conditions on the attributes (in the +where+ method style):
108
150
  #
109
- # Post.exist?(version: 1, 'created_at.gt': Time.now - 1.day)
151
+ # User.exists?(age: 20, 'created_at.gt': Time.now - 1.day)
110
152
  #
153
+ # @param id_or_conditions [String|Array[String]|Array[Array]|Hash] the primary id of the model, a list of primary ids or a hash with the options to filter from.
154
+ # @return [true|false]
111
155
  # @since 0.2.0
112
156
  def exists?(id_or_conditions = {})
113
157
  case id_or_conditions
@@ -122,10 +166,12 @@ module Dynamoid #:nodoc:
122
166
  end
123
167
  end
124
168
 
169
+ # @private
125
170
  def deep_subclasses
126
171
  subclasses + subclasses.map(&:deep_subclasses).flatten
127
172
  end
128
173
 
174
+ # @private
129
175
  def choose_right_class(attrs)
130
176
  attrs[inheritance_field] ? attrs[inheritance_field].constantize : self
131
177
  end
@@ -133,12 +179,20 @@ module Dynamoid #:nodoc:
133
179
 
134
180
  # Initialize a new object.
135
181
  #
136
- # @param [Hash] attrs Attributes with which to create the object.
182
+ # User.new(name: 'A')
137
183
  #
184
+ # Initialize an object and pass it into a block to set other attributes.
185
+ #
186
+ # User.new(name: 'A') do |u|
187
+ # u.age = 21
188
+ # end
189
+ #
190
+ # @param attrs [Hash] Attributes with which to create the document
191
+ # @param block [Proc] Block to process a document after initialization
138
192
  # @return [Dynamoid::Document] the new document
139
193
  #
140
194
  # @since 0.2.0
141
- def initialize(attrs = {})
195
+ def initialize(attrs = {}, &block)
142
196
  run_callbacks :initialize do
143
197
  @new_record = true
144
198
  @attributes ||= {}
@@ -156,11 +210,19 @@ module Dynamoid #:nodoc:
156
210
  attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
157
211
 
158
212
  load(attrs_with_defaults.merge(attrs_virtual))
213
+
214
+ if block
215
+ block.call(self)
216
+ end
159
217
  end
160
218
  end
161
219
 
162
- # An object is equal to another object if their ids are equal.
220
+ # Check equality of two models.
163
221
  #
222
+ # A model is equal to another model only if their primary keys (hash key
223
+ # and optionaly range key) are equal.
224
+ #
225
+ # @return [true|false]
164
226
  # @since 0.2.0
165
227
  def ==(other)
166
228
  if self.class.identity_map_on?
@@ -172,36 +234,54 @@ module Dynamoid #:nodoc:
172
234
  end
173
235
  end
174
236
 
237
+ # Check equality of two models.
238
+ #
239
+ # Works exactly like +==+ does.
240
+ #
241
+ # @return [true|false]
175
242
  def eql?(other)
176
243
  self == other
177
244
  end
178
245
 
246
+ # Generate an Integer hash value for this model.
247
+ #
248
+ # Hash value is based on primary key. So models can be used safely as a
249
+ # +Hash+ keys.
250
+ #
251
+ # @return [Integer]
179
252
  def hash
180
253
  hash_key.hash ^ range_value.hash
181
254
  end
182
255
 
183
- # Return an object's hash key, regardless of what it might be called to the object.
256
+ # Return a model's hash key value.
184
257
  #
185
258
  # @since 0.4.0
186
259
  def hash_key
187
- send(self.class.hash_key)
260
+ self[self.class.hash_key.to_sym]
188
261
  end
189
262
 
190
- # Assign an object's hash key, regardless of what it might be called to the object.
263
+ # Assign a model's hash key value, regardless of what it might be called to
264
+ # the object.
191
265
  #
192
266
  # @since 0.4.0
193
267
  def hash_key=(value)
194
- send("#{self.class.hash_key}=", value)
268
+ self[self.class.hash_key.to_sym] = value
195
269
  end
196
270
 
271
+ # Return a model's range key value.
272
+ #
273
+ # Returns +nil+ if a range key isn't declared for a model.
197
274
  def range_value
198
- if range_key = self.class.range_key
199
- send(range_key)
275
+ if self.class.range_key
276
+ self[self.class.range_key.to_sym]
200
277
  end
201
278
  end
202
279
 
280
+ # Assign a model's range key value.
203
281
  def range_value=(value)
204
- send("#{self.class.range_key}=", value)
282
+ if self.class.range_key
283
+ self[self.class.range_key.to_sym] = value
284
+ end
205
285
  end
206
286
 
207
287
  private
@@ -213,7 +293,7 @@ module Dynamoid #:nodoc:
213
293
  # Evaluates the default value given, this is used by undump
214
294
  # when determining the value of the default given for a field options.
215
295
  #
216
- # @param [Object] :value the attribute's default value
296
+ # @param val [Object] the attribute's default value
217
297
  def evaluate_default_value(val)
218
298
  if val.respond_to?(:call)
219
299
  val.call
@@ -225,3 +305,5 @@ module Dynamoid #:nodoc:
225
305
  end
226
306
  end
227
307
  end
308
+
309
+ ActiveSupport.run_load_hooks(:dynamoid, Dynamoid::Document)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
+ # @private
4
5
  module Dumping
5
6
  def self.dump_attributes(attributes, attributes_options)
6
7
  {}.tap do |h|
@@ -35,6 +36,7 @@ module Dynamoid
35
36
  when :serialized then SerializedDumper
36
37
  when :raw then RawDumper
37
38
  when :boolean then BooleanDumper
39
+ when :binary then BinaryDumper
38
40
  when Class then CustomTypeDumper
39
41
  end
40
42
 
@@ -287,6 +289,13 @@ module Dynamoid
287
289
  end
288
290
  end
289
291
 
292
+ # string -> string
293
+ class BinaryDumper < Base
294
+ def process(value)
295
+ Base64.strict_encode64(value)
296
+ end
297
+ end
298
+
290
299
  # any object -> string
291
300
  class CustomTypeDumper < Base
292
301
  def process(value)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
+ # @private
4
5
  module DynamodbTimeZone
5
6
  def self.in_time_zone(value)
6
7
  case Dynamoid::Config.dynamodb_timezone
@@ -75,5 +75,7 @@ module Dynamoid
75
75
  class InvalidQuery < Error; end
76
76
 
77
77
  class UnsupportedKeyType < Error; end
78
+
79
+ class UnknownAttribute < Error; end
78
80
  end
79
81
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ require 'dynamoid/fields/declare'
4
+
5
+ module Dynamoid
4
6
  # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
5
7
  # specified with field, then they will be ignored.
6
8
  module Fields
7
9
  extend ActiveSupport::Concern
8
10
 
11
+ # @private
9
12
  # Types allowed in indexes:
10
13
  PERMITTED_KEY_TYPES = %i[
11
14
  number
12
15
  integer
13
16
  string
17
+ date
14
18
  datetime
15
19
  serialized
16
20
  ].freeze
@@ -33,51 +37,188 @@ module Dynamoid #:nodoc:
33
37
  module ClassMethods
34
38
  # Specify a field for a document.
35
39
  #
36
- # Its type determines how it is coerced when read in and out of the datastore.
37
- # You can specify :integer, :number, :set, :array, :datetime, :date and :serialized,
40
+ # class User
41
+ # include Dynamoid::Document
42
+ #
43
+ # field :last_name
44
+ # field :age, :integer
45
+ # field :last_sign_in, :datetime
46
+ # end
47
+ #
48
+ # Its type determines how it is coerced when read in and out of the
49
+ # data store. You can specify +string+, +integer+, +number+, +set+, +array+,
50
+ # +map+, +datetime+, +date+, +serialized+, +raw+, +boolean+ and +binary+
38
51
  # or specify a class that defines a serialization strategy.
39
52
  #
53
+ # By default field type is +string+.
54
+ #
55
+ # Set can store elements of the same type only (it's a limitation of
56
+ # DynamoDB itself). If a set should store elements only of some particular
57
+ # type then +of+ option should be specified:
58
+ #
59
+ # field :hobbies, :set, of: :string
60
+ #
61
+ # Only +string+, +integer+, +number+, +date+, +datetime+ and +serialized+
62
+ # element types are supported.
63
+ #
64
+ # Element type can have own options - they should be specified in the
65
+ # form of +Hash+:
66
+ #
67
+ # field :hobbies, :set, of: { serialized: { serializer: JSON } }
68
+ #
69
+ # Array can contain element of different types but if supports the same
70
+ # +of+ option to convert all the provided elements to the declared type.
71
+ #
72
+ # field :rates, :array, of: :number
73
+ #
74
+ # By default +date+ and +datetime+ fields are stored as integer values.
75
+ # The format can be changed to string with option +store_as_string+:
76
+ #
77
+ # field :published_on, :datetime, store_as_string: true
78
+ #
79
+ # Boolean field by default is stored as a string +t+ or +f+. But DynamoDB
80
+ # supports boolean type natively. In order to switch to the native
81
+ # boolean type an option +store_as_native_boolean+ should be specified:
82
+ #
83
+ # field :active, :boolean, store_as_native_boolean: true
84
+ #
85
+ # If you specify the +serialized+ type a value will be serialized to
86
+ # string in Yaml format by default. Custom way to serialize value to
87
+ # string can be specified with +serializer+ option. Custom serializer
88
+ # should have +dump+ and +load+ methods.
89
+ #
40
90
  # If you specify a class for field type, Dynamoid will serialize using
41
- # `dynamoid_dump` or `dump` methods, and load using `dynamoid_load` or `load` methods.
91
+ # +dynamoid_dump+ method and load using +dynamoid_load+ method.
92
+ #
93
+ # Default field type is +string+.
42
94
  #
43
- # Default field type is :string.
95
+ # A field can have a default value. It's assigned at initializing a model
96
+ # if no value is specified:
44
97
  #
45
- # @param [Symbol] name the name of the field
46
- # @param [Symbol] type the type of the field (refer to method description for details)
47
- # @param [Hash] options any additional options for the field
98
+ # field :age, :integer, default: 1
99
+ #
100
+ # If a defautl value should be recalculated every time it can be
101
+ # specified as a callable object (it should implement a +call+ method
102
+ # e.g. +Proc+ object):
103
+ #
104
+ # field :date_of_birth, :date, default: -> { Date.today }
105
+ #
106
+ # For every field Dynamoid creates several methods:
107
+ #
108
+ # * getter
109
+ # * setter
110
+ # * predicate +<name>?+ to check whether a value set
111
+ # * +<name>_before_type_cast?+ to get an original field value before it was type casted
112
+ #
113
+ # It works in the following way:
114
+ #
115
+ # class User
116
+ # include Dynamoid::Document
117
+ #
118
+ # field :age, :integer
119
+ # end
120
+ #
121
+ # user = User.new
122
+ # user.age # => nil
123
+ # user.age? # => false
124
+ #
125
+ # user.age = 20
126
+ # user.age? # => true
127
+ #
128
+ # user.age = '21'
129
+ # user.age # => 21 - integer
130
+ # user.age_before_type_cast # => '21' - string
131
+ #
132
+ # There is also an option +alias+ which allows to use another name for a
133
+ # field:
134
+ #
135
+ # class User
136
+ # include Dynamoid::Document
137
+ #
138
+ # field :firstName, :string, alias: :first_name
139
+ # end
140
+ #
141
+ # user = User.new(firstName: 'Michael')
142
+ # user.firstName # Michael
143
+ # user.first_name # Michael
144
+ #
145
+ # @param name [Symbol] name of the field
146
+ # @param type [Symbol] type of the field (optional)
147
+ # @param options [Hash] any additional options for the field type (optional)
48
148
  #
49
149
  # @since 0.2.0
50
150
  def field(name, type = :string, options = {})
51
- named = name.to_s
52
151
  if type == :float
53
152
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
54
153
  type = :number
55
154
  end
56
- self.attributes = attributes.merge(name => { type: type }.merge(options))
57
155
 
58
- define_attribute_method(name) # Dirty API
59
-
60
- generated_methods.module_eval do
61
- define_method(named) { read_attribute(named) }
62
- define_method("#{named}?") do
63
- value = read_attribute(named)
64
- case value
65
- when true then true
66
- when false, nil then false
67
- else
68
- !value.nil?
69
- end
70
- end
71
- define_method("#{named}=") { |value| write_attribute(named, value) }
72
- define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
73
- end
156
+ Dynamoid::Fields::Declare.new(self, name, type, options).call
74
157
  end
75
158
 
159
+ # Declare a table range key.
160
+ #
161
+ # class User
162
+ # include Dynamoid::Document
163
+ #
164
+ # range :last_name
165
+ # end
166
+ #
167
+ # By default a range key is a string. In order to use any other type it
168
+ # should be specified as a second argument:
169
+ #
170
+ # range :age, :integer
171
+ #
172
+ # Type options can be specified as well:
173
+ #
174
+ # range :date_of_birth, :date, store_as_string: true
175
+ #
176
+ # @param name [Symbol] a range key attribute name
177
+ # @param type [Symbol] a range key type (optional)
178
+ # @param options [Symbol] type options (optional)
76
179
  def range(name, type = :string, options = {})
77
180
  field(name, type, options)
78
181
  self.range_key = name
79
182
  end
80
183
 
184
+ # Set table level properties.
185
+ #
186
+ # There are some sensible defaults:
187
+ #
188
+ # * table name is based on a model class e.g. +users+ for +User+ class
189
+ # * hash key name - +id+ by default
190
+ # * hash key type - +string+ by default
191
+ # * generating timestamp fields +created_at+ and +updated_at+
192
+ # * billing mode and read/write capacity units
193
+ #
194
+ # The +table+ method can be used to override the defaults:
195
+ #
196
+ # class User
197
+ # include Dynamoid::Document
198
+ #
199
+ # table name: :customers, key: :uuid
200
+ # end
201
+ #
202
+ # The hash key field is declared by default and a type is a string. If
203
+ # another type is needed the field should be declared explicitly:
204
+ #
205
+ # class User
206
+ # include Dynamoid::Document
207
+ #
208
+ # field :id, :integer
209
+ # end
210
+ #
211
+ # @param options [Hash] options to override default table settings
212
+ # @option options [Symbol] :name name of a table
213
+ # @option options [Symbol] :key name of a hash key attribute
214
+ # @option options [Symbol] :inheritance_field name of an attribute used for STI
215
+ # @option options [Symbol] :capacity_mode table billing mode - either +provisioned+ or +on_demand+
216
+ # @option options [Integer] :write_capacity table write capacity units
217
+ # @option options [Integer] :read_capacity table read capacity units
218
+ # @option options [true|false] :timestamps whether generate +created_at+ and +updated_at+ fields or not
219
+ # @option options [Hash] :expires set up a table TTL and should have following structure +{ field: <attriubute name>, after: <seconds> }+
220
+ #
221
+ # @since 0.4.0
81
222
  def table(options)
82
223
  # a default 'id' column is created when Dynamoid::Document is included
83
224
  unless attributes.key? hash_key
@@ -98,6 +239,12 @@ module Dynamoid #:nodoc:
98
239
  end
99
240
  end
100
241
 
242
+ # Remove a field declaration
243
+ #
244
+ # Removes a field from the list of fields and removes all te generated
245
+ # for a field methods.
246
+ #
247
+ # @param field [Symbol] a field name
101
248
  def remove_field(field)
102
249
  field = field.to_sym
103
250
  attributes.delete(field) || raise('No such field')
@@ -114,12 +261,12 @@ module Dynamoid #:nodoc:
114
261
  end
115
262
  end
116
263
 
264
+ # @private
117
265
  def timestamps_enabled?
118
266
  options[:timestamps] || (options[:timestamps].nil? && Dynamoid::Config.timestamps)
119
267
  end
120
268
 
121
- private
122
-
269
+ # @private
123
270
  def generated_methods
124
271
  @generated_methods ||= begin
125
272
  Module.new.tap do |mod|
@@ -133,15 +280,25 @@ module Dynamoid #:nodoc:
133
280
  attr_accessor :attributes
134
281
  alias raw_attributes attributes
135
282
 
136
- # Write an attribute on the object. Also marks the previous value as dirty.
283
+ # Write an attribute on the object.
284
+ #
285
+ # user.age = 20
286
+ # user.write_attribute(:age, 21)
287
+ # user.age # => 21
137
288
  #
138
- # @param [Symbol] name the name of the field
139
- # @param [Object] value the value to assign to that field
289
+ # Also marks the previous value as dirty.
290
+ #
291
+ # @param name [Symbol] the name of the field
292
+ # @param value [Object] the value to assign to that field
140
293
  #
141
294
  # @since 0.2.0
142
295
  def write_attribute(name, value)
143
296
  name = name.to_sym
144
297
 
298
+ unless attribute_is_present_on_model?(name)
299
+ raise Dynamoid::Errors::UnknownAttribute.new("Attribute #{name} is not part of the model")
300
+ end
301
+
145
302
  if association = @associations[name]
146
303
  association.reset
147
304
  end
@@ -157,22 +314,40 @@ module Dynamoid #:nodoc:
157
314
 
158
315
  # Read an attribute from an object.
159
316
  #
160
- # @param [Symbol] name the name of the field
317
+ # user.age = 20
318
+ # user.read_attribute(:age) # => 20
161
319
  #
320
+ # @param name [Symbol] the name of the field
321
+ # @return attribute value
162
322
  # @since 0.2.0
163
323
  def read_attribute(name)
164
324
  attributes[name.to_sym]
165
325
  end
166
326
  alias [] read_attribute
167
327
 
168
- # Returns a hash of attributes before typecasting
328
+ # Return attributes values before type casting.
329
+ #
330
+ # user = User.new
331
+ # user.age = '21'
332
+ # user.age # => 21
333
+ #
334
+ # user.attributes_before_type_cast # => { age: '21' }
335
+ #
336
+ # @return [Hash] original attribute values
169
337
  def attributes_before_type_cast
170
338
  @attributes_before_type_cast
171
339
  end
172
340
 
173
- # Returns the value of the attribute identified by name before typecasting
341
+ # Return the value of the attribute identified by name before type casting.
174
342
  #
175
- # @param [Symbol] attribute name
343
+ # user = User.new
344
+ # user.age = '21'
345
+ # user.age # => 21
346
+ #
347
+ # user.read_attribute_before_type_cast(:age) # => '21'
348
+ #
349
+ # @param name [Symbol] attribute name
350
+ # @return original attribute value
176
351
  def read_attribute_before_type_cast(name)
177
352
  return nil unless name.respond_to?(:to_sym)
178
353
 
@@ -192,7 +367,8 @@ module Dynamoid #:nodoc:
192
367
  #
193
368
  # @since 0.2.0
194
369
  def set_updated_at
195
- if self.class.timestamps_enabled? && !updated_at_changed?
370
+ # @_touch_record=false means explicit disabling
371
+ if self.class.timestamps_enabled? && !updated_at_changed? && @_touch_record != false
196
372
  self.updated_at = DateTime.now.in_time_zone(Time.zone)
197
373
  end
198
374
  end
@@ -219,5 +395,10 @@ module Dynamoid #:nodoc:
219
395
  send("#{type}=", self.class.name)
220
396
  end
221
397
  end
398
+
399
+ def attribute_is_present_on_model?(attribute_name)
400
+ setter = "#{attribute_name}=".to_sym
401
+ respond_to?(setter)
402
+ end
222
403
  end
223
404
  end