dynamoid 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +53 -0
  3. data/.rubocop_todo.yml +55 -0
  4. data/.travis.yml +5 -27
  5. data/Appraisals +17 -15
  6. data/CHANGELOG.md +26 -3
  7. data/Gemfile +4 -2
  8. data/README.md +95 -77
  9. data/Rakefile +17 -17
  10. data/Vagrantfile +5 -3
  11. data/dynamoid.gemspec +39 -45
  12. data/gemfiles/rails_4_2.gemfile +7 -5
  13. data/gemfiles/rails_5_0.gemfile +6 -4
  14. data/gemfiles/rails_5_1.gemfile +6 -4
  15. data/gemfiles/rails_5_2.gemfile +6 -4
  16. data/lib/dynamoid.rb +11 -4
  17. data/lib/dynamoid/adapter.rb +21 -27
  18. data/lib/dynamoid/adapter_plugin/{aws_sdk_v2.rb → aws_sdk_v3.rb} +118 -113
  19. data/lib/dynamoid/application_time_zone.rb +27 -0
  20. data/lib/dynamoid/associations.rb +3 -6
  21. data/lib/dynamoid/associations/association.rb +3 -6
  22. data/lib/dynamoid/associations/belongs_to.rb +4 -5
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -3
  24. data/lib/dynamoid/associations/has_many.rb +2 -3
  25. data/lib/dynamoid/associations/has_one.rb +2 -3
  26. data/lib/dynamoid/associations/many_association.rb +8 -9
  27. data/lib/dynamoid/associations/single_association.rb +3 -3
  28. data/lib/dynamoid/components.rb +2 -2
  29. data/lib/dynamoid/config.rb +9 -5
  30. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +4 -2
  31. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +3 -1
  32. data/lib/dynamoid/config/options.rb +4 -4
  33. data/lib/dynamoid/criteria.rb +3 -5
  34. data/lib/dynamoid/criteria/chain.rb +42 -49
  35. data/lib/dynamoid/dirty.rb +5 -4
  36. data/lib/dynamoid/document.rb +142 -36
  37. data/lib/dynamoid/dumping.rb +167 -0
  38. data/lib/dynamoid/dynamodb_time_zone.rb +16 -0
  39. data/lib/dynamoid/errors.rb +7 -6
  40. data/lib/dynamoid/fields.rb +24 -23
  41. data/lib/dynamoid/finders.rb +101 -59
  42. data/lib/dynamoid/identity_map.rb +5 -11
  43. data/lib/dynamoid/indexes.rb +45 -46
  44. data/lib/dynamoid/middleware/identity_map.rb +2 -0
  45. data/lib/dynamoid/persistence.rb +67 -307
  46. data/lib/dynamoid/primary_key_type_mapping.rb +34 -0
  47. data/lib/dynamoid/railtie.rb +3 -1
  48. data/lib/dynamoid/tasks/database.rake +11 -11
  49. data/lib/dynamoid/tasks/database.rb +4 -3
  50. data/lib/dynamoid/type_casting.rb +193 -0
  51. data/lib/dynamoid/undumping.rb +188 -0
  52. data/lib/dynamoid/validations.rb +4 -7
  53. data/lib/dynamoid/version.rb +3 -1
  54. metadata +59 -53
  55. data/gemfiles/rails_4_0.gemfile +0 -9
  56. data/gemfiles/rails_4_1.gemfile +0 -9
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ class PrimaryKeyTypeMapping
5
+ def self.dynamodb_type(type, options)
6
+ if Class === type
7
+ type = type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
8
+ end
9
+
10
+ case type
11
+ when :string, :serialized
12
+ :string
13
+ when :integer, :number
14
+ :number
15
+ when :datetime
16
+ string_format = if options[:store_as_string].nil?
17
+ Dynamoid::Config.store_datetime_as_string
18
+ else
19
+ options[:store_as_string]
20
+ end
21
+ string_format ? :string : :number
22
+ when :date
23
+ string_format = if options[:store_as_string].nil?
24
+ Dynamoid::Config.store_date_as_string
25
+ else
26
+ options[:store_as_string]
27
+ end
28
+ string_format ? :string : :number
29
+ else
30
+ raise Errors::UnsupportedKeyType, "#{type} cannot be used as a type of table key attribute"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,6 @@
1
- if defined? (Rails)
1
+ # frozen_string_literal: true
2
+
3
+ if defined? Rails
2
4
 
3
5
  module Dynamoid
4
6
  class Railtie < Rails::Railtie
@@ -1,29 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dynamoid'
2
4
  require 'dynamoid/tasks/database'
3
5
 
4
6
  namespace :dynamoid do
5
- desc "Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables"
6
- task :create_tables => :environment do
7
+ desc 'Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables'
8
+ task create_tables: :environment do
7
9
  # Load models so Dynamoid will be able to discover tables expected.
8
- Dir[ File.join(Dynamoid::Config.models_dir, "*.rb") ].sort.each { |file| require file }
10
+ Dir[File.join(Dynamoid::Config.models_dir, '*.rb')].sort.each { |file| require file }
9
11
  if Dynamoid.included_models.any?
10
12
  tables = Dynamoid::Tasks::Database.create_tables
11
- result = tables[:created].map{ |c| "#{c} created" } + tables[:existing].map{ |e| "#{e} already exists" }
12
- result.sort.each{ |r| puts r }
13
+ result = tables[:created].map { |c| "#{c} created" } + tables[:existing].map { |e| "#{e} already exists" }
14
+ result.sort.each { |r| puts r }
13
15
  else
14
- puts "Dynamoid models are not loaded, or you have no Dynamoid models."
16
+ puts 'Dynamoid models are not loaded, or you have no Dynamoid models.'
15
17
  end
16
18
  end
17
19
 
18
20
  desc 'Tests if the DynamoDB instance can be contacted using your configuration'
19
- task :ping => :environment do
21
+ task ping: :environment do
20
22
  success = false
21
23
  failure_reason = nil
22
24
 
23
25
  begin
24
26
  Dynamoid::Tasks::Database.ping
25
27
  success = true
26
- rescue Exception => e
28
+ rescue StandardError => e
27
29
  failure_reason = e.message
28
30
  end
29
31
 
@@ -33,9 +35,7 @@ namespace :dynamoid do
33
35
  else
34
36
  ' at remote AWS endpoint'
35
37
  end
36
- if not success
37
- msg << ", reason being '#{failure_reason}'"
38
- end
38
+ msg << ", reason being '#{failure_reason}'" unless success
39
39
  puts msg
40
40
  end
41
41
  end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynamoid
2
4
  module Tasks
3
5
  module Database
4
- extend self
6
+ module_function
5
7
 
6
8
  # Create any new tables for the models. Existing tables are not
7
9
  # modified.
8
10
  def create_tables
9
11
  results = { created: [], existing: [] }
10
12
  # We can't quite rely on Dynamoid.included_models alone, we need to select only viable models
11
- Dynamoid.included_models.select{ |m| not m.base_class.try(:name).blank? }.uniq(&:table_name).each do |model|
13
+ Dynamoid.included_models.reject { |m| m.base_class.try(:name).blank? }.uniq(&:table_name).each do |model|
12
14
  if Dynamoid.adapter.list_tables.include? model.table_name
13
15
  results[:existing] << model.table_name
14
16
  else
@@ -24,7 +26,6 @@ module Dynamoid
24
26
  Dynamoid.adapter.list_tables
25
27
  true
26
28
  end
27
-
28
29
  end
29
30
  end
30
31
  end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module TypeCasting
5
+ def self.cast_attributes(attributes, attributes_options)
6
+ {}.tap do |h|
7
+ attributes.symbolize_keys.each do |attribute, value|
8
+ h[attribute] = cast_field(value, attributes_options[attribute])
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.cast_field(value, options)
14
+ return value if options.nil?
15
+ return nil if value.nil?
16
+
17
+ type_caster = find_type_caster(options)
18
+ if type_caster.nil?
19
+ raise ArgumentError, "Unknown type #{options[:type]}"
20
+ end
21
+
22
+ type_caster.process(value)
23
+ end
24
+
25
+ def self.find_type_caster(options)
26
+ type_caster_class = case options[:type]
27
+ when :string then StringTypeCaster
28
+ when :integer then IntegerTypeCaster
29
+ when :number then NumberTypeCaster
30
+ when :set then SetTypeCaster
31
+ when :array then ArrayTypeCaster
32
+ when :datetime then DateTimeTypeCaster
33
+ when :date then DateTypeCaster
34
+ when :raw then RawTypeCaster
35
+ when :serialized then SerializedTypeCaster
36
+ when :boolean then BooleanTypeCaster
37
+ when Class then CustomTypeCaster
38
+ end
39
+
40
+ if type_caster_class.present?
41
+ type_caster_class.new(options)
42
+ end
43
+ end
44
+
45
+ class Base
46
+ def initialize(options)
47
+ @options = options
48
+ end
49
+
50
+ def process(value)
51
+ value
52
+ end
53
+ end
54
+
55
+ class StringTypeCaster < Base
56
+ def process(value)
57
+ if value == true
58
+ 't'
59
+ elsif value == false
60
+ 'f'
61
+ elsif value.is_a? String
62
+ value.dup
63
+ else
64
+ value.to_s
65
+ end
66
+ end
67
+ end
68
+
69
+ class IntegerTypeCaster < Base
70
+ def process(value)
71
+ if value == true
72
+ 1
73
+ elsif value == false
74
+ 0
75
+ elsif value.is_a?(String) && value.blank?
76
+ nil
77
+ elsif value.is_a?(Float) && !value.finite?
78
+ nil
79
+ elsif !value.respond_to?(:to_i)
80
+ nil
81
+ else
82
+ value.to_i
83
+ end
84
+ end
85
+ end
86
+
87
+ class NumberTypeCaster < Base
88
+ def process(value)
89
+ if value == true
90
+ 1
91
+ elsif value == false
92
+ 0
93
+ elsif value.is_a?(Symbol)
94
+ value.to_s.to_d
95
+ elsif value.is_a?(String) && value.blank?
96
+ nil
97
+ elsif value.is_a?(Float) && !value.finite?
98
+ nil
99
+ elsif !(value.respond_to?(:to_d))
100
+ nil
101
+ else
102
+ value.to_d
103
+ end
104
+ end
105
+ end
106
+
107
+ class SetTypeCaster < Base
108
+ def process(value)
109
+ if value.is_a?(Set)
110
+ value.dup
111
+ elsif value.respond_to?(:to_set)
112
+ value.to_set
113
+ else
114
+ nil
115
+ end
116
+ end
117
+ end
118
+
119
+ class ArrayTypeCaster < Base
120
+ def process(value)
121
+ if value.is_a?(Array)
122
+ value.dup
123
+ elsif value.respond_to?(:to_a)
124
+ value.to_a
125
+ else
126
+ nil
127
+ end
128
+ end
129
+ end
130
+
131
+ class DateTimeTypeCaster < Base
132
+ def process(value)
133
+ if !value.respond_to?(:to_datetime)
134
+ nil
135
+ elsif value.is_a?(String)
136
+ dt = DateTime.parse(value) rescue nil
137
+ if dt
138
+ seconds = string_utc_offset(value) || ApplicationTimeZone.utc_offset
139
+ offset = seconds_to_offset(seconds)
140
+ DateTime.new(dt.year, dt.mon, dt.mday, dt.hour, dt.min, dt.sec, offset)
141
+ end
142
+ else
143
+ value.to_datetime
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def string_utc_offset(string)
150
+ Date._parse(string)[:offset]
151
+ end
152
+
153
+ # 3600 -> "+01:00"
154
+ def seconds_to_offset(seconds)
155
+ ActiveSupport::TimeZone.seconds_to_utc_offset(seconds)
156
+ end
157
+ end
158
+
159
+ class DateTypeCaster < Base
160
+ def process(value)
161
+ if !value.respond_to?(:to_date)
162
+ nil
163
+ else
164
+ begin
165
+ value.to_date
166
+ rescue ArgumentError
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ class RawTypeCaster < Base
173
+ end
174
+
175
+ class SerializedTypeCaster < Base
176
+ end
177
+
178
+ class BooleanTypeCaster < Base
179
+ def process(value)
180
+ if value == ''
181
+ nil
182
+ elsif [false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF'].include? value
183
+ false
184
+ else
185
+ true
186
+ end
187
+ end
188
+ end
189
+
190
+ class CustomTypeCaster < Base
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Undumping
5
+ def self.undump_attributes(attributes, attributes_options)
6
+ {}.tap do |h|
7
+ attributes.symbolize_keys.each do |attribute, value|
8
+ h[attribute] = undump_field(value, attributes_options[attribute])
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.undump_field(value, options)
14
+ undumper = find_undumper(options)
15
+
16
+ if undumper.nil?
17
+ raise ArgumentError, "Unknown type #{options[:type]}"
18
+ end
19
+
20
+ return nil if value.nil?
21
+ undumper.process(value)
22
+ end
23
+
24
+ def self.find_undumper(options)
25
+ undumper_class = case options[:type]
26
+ when :string then StringUndumper
27
+ when :integer then IntegerUndumper
28
+ when :number then NumberUndumper
29
+ when :set then SetUndumper
30
+ when :array then ArrayUndumper
31
+ when :datetime then DateTimeUndumper
32
+ when :date then DateUndumper
33
+ when :raw then RawUndumper
34
+ when :serialized then SerializedUndumper
35
+ when :boolean then BooleanUndumper
36
+ when Class then CustomTypeUndumper
37
+ end
38
+
39
+ if undumper_class.present?
40
+ undumper_class.new(options)
41
+ end
42
+ end
43
+
44
+ class Base
45
+ def initialize(options)
46
+ @options = options
47
+ end
48
+
49
+ def process(value)
50
+ value
51
+ end
52
+ end
53
+
54
+ class StringUndumper < Base
55
+ end
56
+
57
+ class IntegerUndumper < Base
58
+ def process(value)
59
+ value.to_i
60
+ end
61
+ end
62
+
63
+ class NumberUndumper < Base
64
+ end
65
+
66
+ class SetUndumper < Base
67
+ def process(value)
68
+ case @options[:of]
69
+ when :integer
70
+ value.map { |v| Integer(v) }.to_set
71
+ when :number
72
+ value.map { |v| BigDecimal(v.to_s) }.to_set
73
+ else
74
+ value.is_a?(Set) ? value : Set.new(value)
75
+ end
76
+ end
77
+ end
78
+
79
+ class ArrayUndumper < Base
80
+ end
81
+
82
+ class DateTimeUndumper < Base
83
+ def process(value)
84
+ return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
85
+
86
+ use_string_format = if @options[:store_as_string].nil?
87
+ Dynamoid.config.store_datetime_as_string
88
+ else
89
+ @options[:store_as_string]
90
+ end
91
+ value = DateTime.iso8601(value).to_time.to_i if use_string_format
92
+ ApplicationTimeZone.at(value)
93
+ end
94
+ end
95
+
96
+ class DateUndumper < Base
97
+ def process(value)
98
+ use_string_format = if @options[:store_as_string].nil?
99
+ Dynamoid.config.store_date_as_string
100
+ else
101
+ @options[:store_as_string]
102
+ end
103
+
104
+ if use_string_format
105
+ Date.iso8601(value)
106
+ else
107
+ Dynamoid::Persistence::UNIX_EPOCH_DATE + value.to_i
108
+ end
109
+ end
110
+ end
111
+
112
+ class RawUndumper < Base
113
+ def process(value)
114
+ if value.is_a?(Hash)
115
+ undump_hash(value)
116
+ else
117
+ value
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def undump_hash(hash)
124
+ {}.tap do |h|
125
+ hash.each { |key, value| h[key.to_sym] = undump_hash_value(value) }
126
+ end
127
+ end
128
+
129
+ def undump_hash_value(val)
130
+ case val
131
+ when BigDecimal
132
+ if Dynamoid::Config.convert_big_decimal
133
+ val.to_f
134
+ else
135
+ val
136
+ end
137
+ when Hash
138
+ undump_hash(val)
139
+ when Array
140
+ val.map { |v| undump_hash_value(v) }
141
+ else
142
+ val
143
+ end
144
+ end
145
+ end
146
+
147
+ class SerializedUndumper < Base
148
+ def process(value)
149
+ if @options[:serializer]
150
+ @options[:serializer].load(value)
151
+ else
152
+ YAML.load(value)
153
+ end
154
+ end
155
+ end
156
+
157
+ class BooleanUndumper < Base
158
+ STRING_VALUES = ['t', 'f']
159
+
160
+ def process(value)
161
+ store_as_boolean = if @options[:store_as_native_boolean].nil?
162
+ Dynamoid.config.store_boolean_as_native
163
+ else
164
+ @options[:store_as_native_boolean]
165
+ end
166
+ if store_as_boolean
167
+ !!value
168
+ elsif STRING_VALUES.include?(value)
169
+ value == 't'
170
+ else
171
+ raise ArgumentError, 'Boolean column neither true nor false'
172
+ end
173
+ end
174
+ end
175
+
176
+ class CustomTypeUndumper < Base
177
+ def process(value)
178
+ field_class = @options[:type]
179
+
180
+ unless field_class.respond_to?(:dynamoid_load)
181
+ raise ArgumentError, "#{field_class} does not support serialization for Dynamoid."
182
+ end
183
+
184
+ field_class.dynamoid_load(value)
185
+ end
186
+ end
187
+ end
188
+ end