ocean-dynamo 0.1.10 → 0.1.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 73851353b9d6b8bee8d39d16f43cf6006026260f
4
- data.tar.gz: d0d20d6ec619909fad2b35e46294be360c964dbd
3
+ metadata.gz: 2715c40f4987fe7ffe19eecd488bc67b5a9cebb3
4
+ data.tar.gz: 8fdae13f4f5d71632a0c938588e977807a53ec61
5
5
  SHA512:
6
- metadata.gz: b41c394e855a63e1afc69968b97a807044ad8cde207607aa77ae63300f121f0b733e463579727d92029d65a5916663f41fe630fc089ad78bd9591214f752d8c7
7
- data.tar.gz: 9ecb8bb8b155e5a9aa9f92a8a53675b38e2336df9b5381cfd5242ba0354b60b4f0e1616a2ce377e3e6447509cd5ebdbf65056642f7717f0e43b8c520ea112b6b
6
+ metadata.gz: b400e19b2a3b2707adfea3e415fe672352fe0e30267992ce14e70b616a7bce0ee1a569396278e0fe675ec2f4a7a4db65e4e7fd922ecd6d5112d7ce4aa5eff240
7
+ data.tar.gz: a0cc3aa3eb5866d64db1407267e5c9ac63606452eaeda01eb766f24888d6546b33768b80bcb3df6ccf8851f45876106a660026dee48ef7df6e3cd3b151c54610
@@ -1,9 +1,24 @@
1
1
  require "ocean-dynamo/engine"
2
2
 
3
- require "ocean-dynamo/dynamo"
3
+ require "aws-sdk"
4
+
5
+ require "ocean-dynamo/base"
6
+ require "ocean-dynamo/exceptions"
7
+ require "ocean-dynamo/class_variables"
8
+ require "ocean-dynamo/tables"
9
+ require "ocean-dynamo/schema"
10
+ require "ocean-dynamo/callbacks"
11
+ require "ocean-dynamo/attributes"
12
+ require "ocean-dynamo/queries"
13
+ require "ocean-dynamo/persistence"
4
14
 
5
15
 
6
16
  module OceanDynamo
7
17
 
8
-
18
+ DEFAULT_ATTRIBUTES = [
19
+ [:created_at, :datetime],
20
+ [:updated_at, :datetime],
21
+ [:lock_version, :integer, default: 0]
22
+ ]
23
+
9
24
  end
@@ -0,0 +1,230 @@
1
+ module OceanDynamo
2
+ class Base
3
+ include ActiveModel::DeprecatedMassAssignmentSecurity
4
+ include ActiveModel::ForbiddenAttributesProtection
5
+
6
+ attr_reader :attributes
7
+ attr_reader :destroyed
8
+ attr_reader :new_record
9
+ attr_reader :dynamo_item
10
+
11
+
12
+ def initialize(attrs={})
13
+ run_callbacks :initialize do
14
+ @attributes = Hash.new
15
+ fields.each do |name, md|
16
+ write_attribute(name, evaluate_default(md[:default], md[:type]))
17
+ self.class.class_eval "def #{name}; read_attribute('#{name.to_s}'); end"
18
+ self.class.class_eval "def #{name}=(value); write_attribute('#{name.to_s}', value); end"
19
+ if fields[name][:type] == :boolean
20
+ self.class.class_eval "def #{name}?; read_attribute('#{name.to_s}'); end"
21
+ end
22
+ end
23
+ @dynamo_item = nil
24
+ @destroyed = false
25
+ @new_record = true
26
+ raise UnknownPrimaryKey unless table_hash_key
27
+ end
28
+ attrs && attrs.delete_if { |k, v| !fields.has_key?(k) }
29
+ super(attrs)
30
+ end
31
+
32
+
33
+ def read_attribute_for_validation(key)
34
+ @attributes[key.to_s]
35
+ end
36
+
37
+
38
+ def read_attribute(attr_name)
39
+ attr_name = attr_name.to_s
40
+ if attr_name == 'id' && fields[table_hash_key] != attr_name.to_sym
41
+ return read_attribute(table_hash_key)
42
+ end
43
+ @attributes[attr_name] # Type cast!
44
+ end
45
+
46
+
47
+ def write_attribute(attr_name, value)
48
+ attr_name = attr_name.to_s
49
+ attr_name = table_hash_key.to_s if attr_name == 'id' && fields[table_hash_key]
50
+ if fields.has_key?(attr_name)
51
+ @attributes[attr_name] = type_cast_attribute_for_write(attr_name, value)
52
+ else
53
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
54
+ end
55
+ end
56
+
57
+
58
+ def type_cast_attribute_for_write(name, value, metadata=fields[name],
59
+ type: metadata[:type])
60
+ case type
61
+ when :string
62
+ return nil if value == nil
63
+ return value.collect(&:to_s) if value.is_a?(Array)
64
+ value
65
+ when :integer
66
+ return nil if value == nil
67
+ return value.collect(&:to_i) if value.is_a?(Array)
68
+ value.to_i
69
+ when :float
70
+ return nil if value == nil
71
+ return value.collect(&:to_f) if value.is_a?(Array)
72
+ value.to_f
73
+ when :boolean
74
+ return nil if value == nil
75
+ return true if value == true
76
+ return true if value == "true"
77
+ false
78
+ when :datetime
79
+ return nil if value == nil || !value.kind_of?(Time)
80
+ value
81
+ when :serialized
82
+ return nil if value == nil
83
+ value
84
+ else
85
+ raise UnsupportedType.new(type.to_s)
86
+ end
87
+ end
88
+
89
+
90
+ def serialized_attributes
91
+ result = {}
92
+ fields.each do |attribute, metadata|
93
+ serialized = serialize_attribute(attribute, read_attribute(attribute), metadata)
94
+ result[attribute] = serialized unless serialized == nil
95
+ end
96
+ result
97
+ end
98
+
99
+
100
+ def serialize_attribute(attribute, value, metadata=fields[attribute],
101
+ type: metadata[:type])
102
+ return nil if value == nil
103
+ case type
104
+ when :string
105
+ ["", []].include?(value) ? nil : value
106
+ when :integer
107
+ value == [] ? nil : value
108
+ when :float
109
+ value == [] ? nil : value
110
+ when :boolean
111
+ value ? "true" : "false"
112
+ when :datetime
113
+ value.to_i
114
+ when :serialized
115
+ value.to_json
116
+ else
117
+ raise UnsupportedType.new(type.to_s)
118
+ end
119
+ end
120
+
121
+
122
+ def deserialized_attributes(consistent_read: false, hash: nil)
123
+ hash ||= dynamo_item.attributes.to_hash(consistent_read: consistent_read)
124
+ result = {}
125
+ fields.each do |attribute, metadata|
126
+ result[attribute] = deserialize_attribute(hash[attribute], metadata)
127
+ end
128
+ result
129
+ end
130
+
131
+
132
+ def deserialize_attribute(value, metadata, type: metadata[:type])
133
+ case type
134
+ when :string
135
+ return "" if value == nil
136
+ value.is_a?(Set) ? value.to_a : value
137
+ when :integer
138
+ return nil if value == nil
139
+ value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_i) : value.to_i
140
+ when :float
141
+ return nil if value == nil
142
+ value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_f) : value.to_f
143
+ when :boolean
144
+ case value
145
+ when "true"
146
+ true
147
+ when "false"
148
+ false
149
+ else
150
+ nil
151
+ end
152
+ when :datetime
153
+ return nil if value == nil
154
+ Time.zone.at(value.to_i)
155
+ when :serialized
156
+ return nil if value == nil
157
+ JSON.parse(value)
158
+ else
159
+ raise UnsupportedType.new(type.to_s)
160
+ end
161
+ end
162
+
163
+
164
+ def [](attribute)
165
+ read_attribute attribute
166
+ end
167
+
168
+
169
+ def []=(attribute, value)
170
+ write_attribute attribute, value
171
+ end
172
+
173
+
174
+ def id
175
+ read_attribute(table_hash_key)
176
+ end
177
+
178
+
179
+ def id=(value)
180
+ write_attribute(table_hash_key, value)
181
+ end
182
+
183
+
184
+ def to_key
185
+ return nil unless persisted?
186
+ key = respond_to?(:id) && id
187
+ return nil unless key
188
+ table_range_key ? [key, read_attribute(table_range_key)] : [key]
189
+ end
190
+
191
+
192
+ def assign_attributes(values)
193
+ return if values.blank?
194
+ values = values.stringify_keys
195
+ # if values.respond_to?(:permitted?)
196
+ # unless values.permitted?
197
+ # raise ActiveModel::ForbiddenAttributesError
198
+ # end
199
+ # end
200
+ values.each do |k, v|
201
+ #send("#{k}=", v)
202
+ _assign_attribute(k, v)
203
+ end
204
+ end
205
+
206
+
207
+ private
208
+
209
+ def _assign_attribute(k, v)
210
+ public_send("#{k}=", v)
211
+ rescue ActiveModel::NoMethodError
212
+ if respond_to?("#{k}=")
213
+ raise
214
+ else
215
+ raise ActiveModel::UnknownAttributeError, "unknown attribute: #{k}"
216
+ end
217
+ end
218
+
219
+
220
+ protected
221
+
222
+ def evaluate_default(default, type)
223
+ return default.call if default.is_a?(Proc)
224
+ return "" if default == nil && type == :string
225
+ return default.clone if default.is_a?(Array) || default.is_a?(String) # Instances need their own copies
226
+ default
227
+ end
228
+
229
+ end
230
+ end
@@ -0,0 +1,8 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ include ActiveModel::Model
5
+ include ActiveModel::Validations::Callbacks
6
+
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ include ActiveModel::Validations::Callbacks
5
+
6
+ define_model_callbacks :initialize, only: :after
7
+ define_model_callbacks :save
8
+ define_model_callbacks :create
9
+ define_model_callbacks :update
10
+ define_model_callbacks :destroy
11
+ define_model_callbacks :commit, only: :after
12
+ define_model_callbacks :touch
13
+
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ class_attribute :dynamo_client, instance_writer: false
5
+ self.dynamo_client = nil
6
+
7
+ class_attribute :dynamo_table, instance_writer: false
8
+ self.dynamo_table = nil
9
+
10
+ class_attribute :dynamo_items, instance_writer: false
11
+ self.dynamo_items = nil
12
+
13
+ class_attribute :table_name, instance_writer: false
14
+ self.table_name = nil
15
+
16
+ class_attribute :table_name_prefix, instance_writer: false
17
+ self.table_name_prefix = nil
18
+
19
+ class_attribute :table_name_suffix, instance_writer: false
20
+ self.table_name_suffix = nil
21
+
22
+ class_attribute :table_hash_key, instance_writer: false
23
+ self.table_hash_key = nil
24
+
25
+ class_attribute :table_range_key, instance_writer: false
26
+ self.table_range_key = nil
27
+
28
+ class_attribute :table_read_capacity_units, instance_writer: false
29
+ self.table_read_capacity_units = 10
30
+
31
+ class_attribute :table_write_capacity_units, instance_writer: false
32
+ self.table_write_capacity_units = 5
33
+
34
+ class_attribute :fields, instance_writer: false
35
+ self.fields = nil
36
+
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ module OceanDynamo
2
+
3
+ class DynamoError < StandardError; end
4
+
5
+ class UnknownPrimaryKey < DynamoError; end
6
+
7
+ class UnknownTableStatus < DynamoError; end
8
+
9
+ class UnsupportedType < DynamoError; end
10
+
11
+ class SerializationTypeMismatch < DynamoError; end
12
+
13
+ class ConnectionNotEstablished < DynamoError; end
14
+
15
+ class RecordNotFound < DynamoError; end
16
+
17
+ class RecordNotSaved < DynamoError; end
18
+
19
+ class RecordInvalid < DynamoError; end
20
+
21
+ class RecordNotDestroyed < DynamoError; end
22
+
23
+ class StatementInvalid < DynamoError; end
24
+ class RecordNotUnique < StatementInvalid; end
25
+ class InvalidForeignKey < StatementInvalid; end
26
+
27
+ class StaleObjectError < DynamoError; end
28
+
29
+ class ReadOnlyRecord < DynamoError; end
30
+
31
+ class DangerousAttributeError < DynamoError; end
32
+
33
+ class UnknownAttributeError < NoMethodError; end
34
+
35
+ class AttributeAssignmentError < DynamoError; end
36
+
37
+ class MultiparameterAssignmentErrors < DynamoError; end
38
+
39
+ end
@@ -0,0 +1,202 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ # ---------------------------------------------------------
5
+ #
6
+ # Class methods
7
+ #
8
+ # ---------------------------------------------------------
9
+
10
+
11
+ def self.create(attributes = nil, &block)
12
+ object = new(attributes)
13
+ yield(object) if block_given?
14
+ object.save
15
+ object
16
+ end
17
+
18
+
19
+ def self.create!(attributes = nil, &block)
20
+ object = new(attributes)
21
+ yield(object) if block_given?
22
+ object.save!
23
+ object
24
+ end
25
+
26
+
27
+ def self.delete(hash, range=nil)
28
+ item = dynamo_items[hash, range]
29
+ return false unless item.exists?
30
+ item.delete
31
+ true
32
+ end
33
+
34
+
35
+ # ---------------------------------------------------------
36
+ #
37
+ # Instance variables and methods
38
+ #
39
+ # ---------------------------------------------------------
40
+
41
+ def destroyed?
42
+ @destroyed
43
+ end
44
+
45
+
46
+ def new_record?
47
+ @new_record
48
+ end
49
+
50
+
51
+ def persisted?
52
+ !(new_record? || destroyed?)
53
+ end
54
+
55
+
56
+ def valid?(context = nil)
57
+ context ||= (new_record? ? :create : :update)
58
+ output = super(context)
59
+ errors.empty? && output
60
+ end
61
+
62
+
63
+ def save
64
+ begin
65
+ create_or_update
66
+ rescue RecordInvalid
67
+ false
68
+ end
69
+ end
70
+
71
+
72
+ def save!(*)
73
+ create_or_update || raise(RecordNotSaved)
74
+ end
75
+
76
+
77
+ def update_attributes(attributes={})
78
+ assign_attributes(attributes)
79
+ save
80
+ end
81
+
82
+
83
+ def update_attributes!(attributes={})
84
+ assign_attributes(attributes)
85
+ save!
86
+ end
87
+
88
+
89
+ def create_or_update
90
+ result = new_record? ? create : update
91
+ result != false
92
+ end
93
+
94
+
95
+ def create
96
+ return false unless valid?(:create)
97
+ run_callbacks :commit do
98
+ run_callbacks :save do
99
+ run_callbacks :create do
100
+ k = read_attribute(table_hash_key)
101
+ write_attribute(table_hash_key, SecureRandom.uuid) if k == "" || k == nil
102
+ t = Time.zone.now
103
+ self.created_at ||= t
104
+ self.updated_at ||= t
105
+ dynamo_persist
106
+ true
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ def update
114
+ return false unless valid?(:update)
115
+ run_callbacks :commit do
116
+ run_callbacks :save do
117
+ run_callbacks :update do
118
+ self.updated_at = Time.zone.now
119
+ dynamo_persist
120
+ true
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ def destroy
128
+ run_callbacks :commit do
129
+ run_callbacks :destroy do
130
+ delete
131
+ end
132
+ end
133
+ end
134
+
135
+
136
+ def destroy!
137
+ destroy || raise(RecordNotDestroyed)
138
+ end
139
+
140
+
141
+ def delete
142
+ if persisted?
143
+ @dynamo_item.delete
144
+ end
145
+ @destroyed = true
146
+ #freeze
147
+ end
148
+
149
+
150
+ def reload(**keywords)
151
+ range_key = table_range_key && attributes[table_range_key]
152
+ new_instance = self.class.find(id, range_key, **keywords)
153
+ assign_attributes(new_instance.attributes)
154
+ self
155
+ end
156
+
157
+
158
+ def touch(name=nil)
159
+ raise DynamoError, "can not touch on a new record object" unless persisted?
160
+ run_callbacks :touch do
161
+ attrs = ['updated_at']
162
+ attrs << name if name
163
+ t = Time.zone.now
164
+ attrs.each { |k| write_attribute k, t }
165
+ # TODO: handle lock_version
166
+ dynamo_item.attributes.update do |u|
167
+ attrs.each do |k|
168
+ u.set(k => serialize_attribute(k, t))
169
+ end
170
+ end
171
+ self
172
+ end
173
+ end
174
+
175
+
176
+
177
+ protected
178
+
179
+ def perform_validations(options={}) # :nodoc:
180
+ options[:validate] == false || valid?(options[:context])
181
+ end
182
+
183
+
184
+ def dynamo_persist
185
+ @dynamo_item = dynamo_items.put(serialized_attributes)
186
+ @new_record = false
187
+ end
188
+
189
+
190
+ def post_instantiate(item, consistent)
191
+ @dynamo_item = item
192
+ @new_record = false
193
+ assign_attributes(deserialized_attributes(
194
+ hash: nil,
195
+ consistent_read: consistent)
196
+ )
197
+ self
198
+ end
199
+
200
+
201
+ end
202
+ end
@@ -0,0 +1,16 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ def self.find(hash, range=nil, consistent: false)
5
+ item = dynamo_items[hash, range]
6
+ raise RecordNotFound unless item.exists?
7
+ new.send(:post_instantiate, item, consistent)
8
+ end
9
+
10
+
11
+ def self.count
12
+ dynamo_table.item_count || -1 # The || -1 is for fake_dynamo specs.
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,66 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ def self.set_table_name(name)
5
+ self.table_name = name
6
+ true
7
+ end
8
+
9
+
10
+ def self.set_table_name_prefix(prefix)
11
+ self.table_name_prefix = prefix
12
+ true
13
+ end
14
+
15
+
16
+ def self.set_table_name_suffix(suffix)
17
+ self.table_name_suffix = suffix
18
+ true
19
+ end
20
+
21
+
22
+ def self.compute_table_name
23
+ name.pluralize.underscore
24
+ end
25
+
26
+
27
+ def self.table_full_name
28
+ "#{table_name_prefix}#{table_name}#{table_name_suffix}"
29
+ end
30
+
31
+
32
+ #
33
+ # This is where the class is initialized
34
+ #
35
+ def self.primary_key(hash_key, range_key=nil)
36
+ self.dynamo_client = nil
37
+ self.dynamo_table = nil
38
+ self.dynamo_items = nil
39
+ self.table_name = compute_table_name
40
+ self.table_name_prefix = nil
41
+ self.table_name_suffix = nil
42
+ self.fields = HashWithIndifferentAccess.new
43
+ self.table_hash_key = hash_key
44
+ self.table_range_key = range_key
45
+ DEFAULT_ATTRIBUTES.each { |k, name, **pairs| attribute k, name, **pairs }
46
+ nil
47
+ end
48
+
49
+
50
+ def self.read_capacity_units(units)
51
+ self.table_read_capacity_units = units
52
+ end
53
+
54
+
55
+ def self.write_capacity_units(units)
56
+ self.table_write_capacity_units = units
57
+ end
58
+
59
+
60
+ def self.attribute(name, type=:string, **pairs)
61
+ attr_accessor name
62
+ fields[name.to_s] = {type: type, default: pairs[:default]}
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,71 @@
1
+ module OceanDynamo
2
+ class Base
3
+
4
+ def self.establish_db_connection
5
+ setup_dynamo
6
+ if dynamo_table.exists?
7
+ wait_until_table_is_active
8
+ else
9
+ create_table
10
+ end
11
+ set_dynamo_table_keys
12
+ end
13
+
14
+
15
+ def self.setup_dynamo
16
+ #self.dynamo_client = AWS::DynamoDB::Client.new(:api_version => '2012-08-10')
17
+ self.dynamo_client ||= AWS::DynamoDB.new
18
+ self.dynamo_table = dynamo_client.tables[table_full_name]
19
+ self.dynamo_items = dynamo_table.items
20
+ end
21
+
22
+
23
+ def self.wait_until_table_is_active
24
+ loop do
25
+ case dynamo_table.status
26
+ when :active
27
+ set_dynamo_table_keys
28
+ return
29
+ when :updating, :creating
30
+ sleep 1
31
+ next
32
+ when :deleting
33
+ sleep 1 while dynamo_table.exists?
34
+ create_table
35
+ return
36
+ else
37
+ raise UnknownTableStatus.new("Unknown DynamoDB table status '#{dynamo_table.status}'")
38
+ end
39
+ sleep 1
40
+ end
41
+ end
42
+
43
+
44
+ def self.set_dynamo_table_keys
45
+ dynamo_table.hash_key = [table_hash_key, fields[table_hash_key][:type]]
46
+ if table_range_key
47
+ dynamo_table.range_key = [table_range_key, fields[table_range_key][:type]]
48
+ end
49
+ end
50
+
51
+
52
+ def self.create_table
53
+ self.dynamo_table = dynamo_client.tables.create(table_full_name,
54
+ table_read_capacity_units, table_write_capacity_units,
55
+ hash_key: { table_hash_key => fields[table_hash_key][:type]},
56
+ range_key: table_range_key && { table_range_key => fields[table_range_key][:type]}
57
+ )
58
+ sleep 1 until dynamo_table.status == :active
59
+ setup_dynamo
60
+ true
61
+ end
62
+
63
+
64
+ def self.delete_table
65
+ return false unless dynamo_table.exists? && dynamo_table.status == :active
66
+ dynamo_table.delete
67
+ true
68
+ end
69
+
70
+ end
71
+ end
@@ -1,3 +1,3 @@
1
1
  module OceanDynamo
2
- VERSION = "0.1.10"
2
+ VERSION = "0.1.11"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocean-dynamo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Bengtson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-09 00:00:00.000000000 Z
11
+ date: 2013-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -134,8 +134,16 @@ extensions: []
134
134
  extra_rdoc_files: []
135
135
  files:
136
136
  - config/routes.rb
137
- - lib/ocean-dynamo/dynamo.rb
137
+ - lib/ocean-dynamo/attributes.rb
138
+ - lib/ocean-dynamo/base.rb
139
+ - lib/ocean-dynamo/callbacks.rb
140
+ - lib/ocean-dynamo/class_variables.rb
138
141
  - lib/ocean-dynamo/engine.rb
142
+ - lib/ocean-dynamo/exceptions.rb
143
+ - lib/ocean-dynamo/persistence.rb
144
+ - lib/ocean-dynamo/queries.rb
145
+ - lib/ocean-dynamo/schema.rb
146
+ - lib/ocean-dynamo/tables.rb
139
147
  - lib/ocean-dynamo/version.rb
140
148
  - lib/ocean-dynamo.rb
141
149
  - MIT-LICENSE
@@ -1,604 +0,0 @@
1
-
2
- require "aws-sdk"
3
-
4
- module OceanDynamo
5
-
6
- DEFAULT_FIELDS = [
7
- [:created_at, :datetime],
8
- [:updated_at, :datetime],
9
- [:lock_version, :integer, default: 0]
10
- ]
11
-
12
- class DynamoError < StandardError; end
13
-
14
- class UnknownPrimaryKey < DynamoError; end
15
-
16
- class UnknownTableStatus < DynamoError; end
17
-
18
- class UnsupportedType < DynamoError; end
19
-
20
- class SerializationTypeMismatch < DynamoError; end
21
-
22
- class ConnectionNotEstablished < DynamoError; end
23
-
24
- class RecordNotFound < DynamoError; end
25
-
26
- class RecordNotSaved < DynamoError; end
27
-
28
- class RecordInvalid < DynamoError; end
29
-
30
- class RecordNotDestroyed < DynamoError; end
31
-
32
- class StatementInvalid < DynamoError; end
33
-
34
- class WrappedDatabaseException < StatementInvalid; end
35
- class RecordNotUnique < WrappedDatabaseException; end
36
- class InvalidForeignKey < WrappedDatabaseException; end
37
-
38
- class StaleObjectError < DynamoError; end
39
-
40
- class ReadOnlyRecord < DynamoError; end
41
-
42
- class DangerousAttributeError < DynamoError; end
43
-
44
- class UnknownAttributeError < NoMethodError; end
45
-
46
- class AttributeAssignmentError < DynamoError; end
47
-
48
- class MultiparameterAssignmentErrors < DynamoError; end
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
- class Base
57
-
58
- include ActiveModel::Model
59
- include ActiveModel::Validations::Callbacks
60
-
61
-
62
- # ---------------------------------------------------------
63
- #
64
- # Class variables and methods
65
- #
66
- # ---------------------------------------------------------
67
-
68
- class_attribute :dynamo_client, instance_writer: false
69
- self.dynamo_client = nil
70
-
71
- class_attribute :dynamo_table, instance_writer: false
72
- self.dynamo_table = nil
73
-
74
- class_attribute :dynamo_items, instance_writer: false
75
- self.dynamo_items = nil
76
-
77
- class_attribute :table_name, instance_writer: false
78
- self.table_name = nil
79
-
80
- class_attribute :table_name_prefix, instance_writer: false
81
- self.table_name_prefix = nil
82
-
83
- class_attribute :table_name_suffix, instance_writer: false
84
- self.table_name_suffix = nil
85
-
86
- class_attribute :table_hash_key, instance_writer: false
87
- self.table_hash_key = nil
88
-
89
- class_attribute :table_range_key, instance_writer: false
90
- self.table_range_key = nil
91
-
92
- class_attribute :table_read_capacity_units, instance_writer: false
93
- self.table_read_capacity_units = 10
94
-
95
- class_attribute :table_write_capacity_units, instance_writer: false
96
- self.table_write_capacity_units = 5
97
-
98
- class_attribute :fields, instance_writer: false
99
- self.fields = nil
100
-
101
-
102
- def self.set_table_name(name)
103
- self.table_name = name
104
- true
105
- end
106
-
107
-
108
- def self.set_table_name_prefix(prefix)
109
- self.table_name_prefix = prefix
110
- true
111
- end
112
-
113
-
114
- def self.set_table_name_suffix(suffix)
115
- self.table_name_suffix = suffix
116
- true
117
- end
118
-
119
-
120
- def self.compute_table_name
121
- name.pluralize.underscore
122
- end
123
-
124
-
125
- def self.table_full_name
126
- "#{table_name_prefix}#{table_name}#{table_name_suffix}"
127
- end
128
-
129
-
130
- #
131
- # This is where the class is initialized
132
- #
133
- def self.primary_key(hash_key, range_key=nil)
134
- self.dynamo_client = nil
135
- self.dynamo_table = nil
136
- self.dynamo_items = nil
137
- self.table_name = compute_table_name
138
- self.table_name_prefix = nil
139
- self.table_name_suffix = nil
140
- self.fields = HashWithIndifferentAccess.new
141
- self.table_hash_key = hash_key
142
- self.table_range_key = range_key
143
- DEFAULT_FIELDS.each { |k, name, **pairs| attribute k, name, **pairs }
144
- nil
145
- end
146
-
147
-
148
- def self.read_capacity_units(units)
149
- self.table_read_capacity_units = units
150
- end
151
-
152
-
153
- def self.write_capacity_units(units)
154
- self.table_write_capacity_units = units
155
- end
156
-
157
-
158
- def self.attribute(name, type=:string, **pairs)
159
- attr_accessor name
160
- fields[name] = {type: type,
161
- default: pairs[:default]}
162
- end
163
-
164
-
165
- def self.establish_db_connection
166
- setup_dynamo
167
- if dynamo_table.exists?
168
- wait_until_table_is_active
169
- else
170
- create_table
171
- end
172
- set_dynamo_table_keys
173
- end
174
-
175
-
176
- def self.setup_dynamo
177
- #self.dynamo_client = AWS::DynamoDB::Client.new(:api_version => '2012-08-10')
178
- self.dynamo_client ||= AWS::DynamoDB.new
179
- self.dynamo_table = dynamo_client.tables[table_full_name]
180
- self.dynamo_items = dynamo_table.items
181
- end
182
-
183
-
184
- def self.wait_until_table_is_active
185
- loop do
186
- case dynamo_table.status
187
- when :active
188
- set_dynamo_table_keys
189
- return
190
- when :updating, :creating
191
- sleep 1
192
- next
193
- when :deleting
194
- sleep 1 while dynamo_table.exists?
195
- create_table
196
- return
197
- else
198
- raise UnknownTableStatus.new("Unknown DynamoDB table status '#{dynamo_table.status}'")
199
- end
200
- sleep 1
201
- end
202
- end
203
-
204
-
205
- def self.set_dynamo_table_keys
206
- dynamo_table.hash_key = [table_hash_key, fields[table_hash_key][:type]]
207
- if table_range_key
208
- dynamo_table.range_key = [table_range_key, fields[table_range_key][:type]]
209
- end
210
- end
211
-
212
-
213
- def self.create_table
214
- self.dynamo_table = dynamo_client.tables.create(table_full_name,
215
- table_read_capacity_units, table_write_capacity_units,
216
- hash_key: { table_hash_key => fields[table_hash_key][:type]},
217
- range_key: table_range_key && { table_range_key => fields[table_range_key][:type]}
218
- )
219
- sleep 1 until dynamo_table.status == :active
220
- setup_dynamo
221
- true
222
- end
223
-
224
-
225
- def self.delete_table
226
- return false unless dynamo_table.exists? && dynamo_table.status == :active
227
- dynamo_table.delete
228
- true
229
- end
230
-
231
-
232
- def self.create(attributes = nil, &block)
233
- object = new(attributes)
234
- yield(object) if block_given?
235
- object.save
236
- object
237
- end
238
-
239
-
240
- def self.create!(attributes = nil, &block)
241
- object = new(attributes)
242
- yield(object) if block_given?
243
- object.save!
244
- object
245
- end
246
-
247
-
248
- def self.find(hash, range=nil, consistent: false)
249
- item = dynamo_items[hash, range]
250
- raise RecordNotFound unless item.exists?
251
- new.send(:post_instantiate, item, consistent)
252
- end
253
-
254
-
255
- def self.delete(hash, range=nil)
256
- item = dynamo_items[hash, range]
257
- return false unless item.exists?
258
- item.delete
259
- true
260
- end
261
-
262
-
263
- def self.count
264
- dynamo_table.item_count || -1 # The || -1 is for fake_dynamo specs.
265
- end
266
-
267
-
268
- # ---------------------------------------------------------
269
- #
270
- # Callbacks
271
- #
272
- # ---------------------------------------------------------
273
-
274
- define_model_callbacks :initialize, only: :after
275
- define_model_callbacks :save
276
- define_model_callbacks :create
277
- define_model_callbacks :update
278
- define_model_callbacks :destroy
279
- define_model_callbacks :commit, only: :after
280
- define_model_callbacks :touch
281
-
282
-
283
- # ---------------------------------------------------------
284
- #
285
- # Instance variables and methods
286
- #
287
- # ---------------------------------------------------------
288
-
289
- attr_reader :attributes
290
- attr_reader :destroyed
291
- attr_reader :new_record
292
- attr_reader :dynamo_item
293
-
294
-
295
- def initialize(attrs={})
296
- run_callbacks :initialize do
297
- @attributes = HashWithIndifferentAccess.new
298
- fields.each do |name, md|
299
- write_attribute(name, evaluate_default(md[:default], md[:type]))
300
- self.class.class_eval "def #{name}; read_attribute('#{name}'); end"
301
- self.class.class_eval "def #{name}=(value); write_attribute('#{name}', value); end"
302
- if fields[name][:type] == :boolean
303
- self.class.class_eval "def #{name}?; read_attribute('#{name}'); end"
304
- end
305
- end
306
- @dynamo_item = nil
307
- @destroyed = false
308
- @new_record = true
309
- raise UnknownPrimaryKey unless table_hash_key
310
- end
311
- attrs && attrs.delete_if { |k, v| !fields.has_key?(k) }
312
- super(attrs)
313
- end
314
-
315
-
316
- def read_attribute(name)
317
- @attributes[name]
318
- end
319
-
320
-
321
- def write_attribute(name, value)
322
- @attributes[name] = value
323
- end
324
-
325
-
326
- def [](attribute)
327
- read_attribute attribute
328
- end
329
-
330
-
331
- def []=(attribute, value)
332
- write_attribute attribute, value
333
- end
334
-
335
-
336
- def id
337
- read_attribute(table_hash_key)
338
- end
339
-
340
-
341
- def id=(value)
342
- write_attribute(table_hash_key, value)
343
- end
344
-
345
-
346
- def to_key
347
- return nil unless persisted?
348
- key = respond_to?(:id) && id
349
- return nil unless key
350
- table_range_key ? [key, read_attribute(table_range_key)] : [key]
351
- end
352
-
353
-
354
-
355
- def assign_attributes(values)
356
- # if values.respond_to?(:permitted?)
357
- # unless values.permitted?
358
- # raise ActiveModel::ForbiddenAttributesError
359
- # end
360
- # end
361
- values.each do |k, v|
362
- send("#{k}=", v)
363
- end
364
- end
365
-
366
-
367
- def serialized_attributes
368
- result = {}
369
- fields.each do |attribute, metadata|
370
- serialized = serialize_attribute(attribute, read_attribute(attribute), metadata)
371
- result[attribute] = serialized unless serialized == nil
372
- end
373
- result
374
- end
375
-
376
-
377
- def serialize_attribute(attribute, value, metadata=fields[attribute],
378
- type: metadata[:type])
379
- return nil if value == nil
380
- case type
381
- when :string
382
- ["", []].include?(value) ? nil : value
383
- when :integer
384
- value == [] ? nil : value
385
- when :float
386
- value == [] ? nil : value
387
- when :boolean
388
- value ? "true" : "false"
389
- when :datetime
390
- value.to_i
391
- when :serialized
392
- value.to_json
393
- else
394
- raise UnsupportedType.new(type.to_s)
395
- end
396
- end
397
-
398
-
399
- def deserialized_attributes(consistent_read: false, hash: nil)
400
- hash ||= dynamo_item.attributes.to_hash(consistent_read: consistent_read)
401
- result = {}
402
- fields.each do |attribute, metadata|
403
- result[attribute] = deserialize_attribute(hash[attribute], metadata)
404
- end
405
- result
406
- end
407
-
408
-
409
- def deserialize_attribute(value, metadata, type: metadata[:type])
410
- case type
411
- when :string
412
- return "" if value == nil
413
- value.is_a?(Set) ? value.to_a : value
414
- when :integer
415
- return nil if value == nil
416
- value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_i) : value.to_i
417
- when :float
418
- return nil if value == nil
419
- value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_f) : value.to_f
420
- when :boolean
421
- case value
422
- when "true"
423
- true
424
- when "false"
425
- false
426
- else
427
- nil
428
- end
429
- when :datetime
430
- return nil if value == nil
431
- Time.at(value.to_i)
432
- when :serialized
433
- return nil if value == nil
434
- JSON.parse(value)
435
- else
436
- raise UnsupportedType.new(type.to_s)
437
- end
438
- end
439
-
440
-
441
- def destroyed?
442
- @destroyed
443
- end
444
-
445
-
446
- def new_record?
447
- @new_record
448
- end
449
-
450
-
451
- def persisted?
452
- !(new_record? || destroyed?)
453
- end
454
-
455
-
456
- def valid?(context = nil)
457
- context ||= (new_record? ? :create : :update)
458
- output = super(context)
459
- errors.empty? && output
460
- end
461
-
462
-
463
- def save
464
- begin
465
- create_or_update
466
- rescue RecordInvalid
467
- false
468
- end
469
- end
470
-
471
-
472
- def save!(*)
473
- create_or_update || raise(RecordNotSaved)
474
- end
475
-
476
-
477
- def update_attributes(attributes={})
478
- assign_attributes(attributes)
479
- save
480
- end
481
-
482
-
483
- def update_attributes!(attributes={})
484
- assign_attributes(attributes)
485
- save!
486
- end
487
-
488
-
489
- def create_or_update
490
- result = new_record? ? create : update
491
- result != false
492
- end
493
-
494
-
495
- def create
496
- return false unless valid?(:create)
497
- run_callbacks :commit do
498
- run_callbacks :save do
499
- run_callbacks :create do
500
- k = read_attribute(table_hash_key)
501
- write_attribute(table_hash_key, SecureRandom.uuid) if k == "" || k == nil
502
- t = Time.now
503
- self.created_at ||= t
504
- self.updated_at ||= t
505
- dynamo_persist
506
- true
507
- end
508
- end
509
- end
510
- end
511
-
512
-
513
- def update
514
- return false unless valid?(:update)
515
- run_callbacks :commit do
516
- run_callbacks :save do
517
- run_callbacks :update do
518
- self.updated_at = Time.now
519
- dynamo_persist
520
- true
521
- end
522
- end
523
- end
524
- end
525
-
526
- def destroy
527
- run_callbacks :commit do
528
- run_callbacks :destroy do
529
- delete
530
- end
531
- end
532
- end
533
-
534
-
535
- def delete
536
- if persisted?
537
- @dynamo_item.delete
538
- end
539
- @destroyed = true
540
- #freeze
541
- end
542
-
543
-
544
- def reload(**keywords)
545
- range_key = table_range_key && attributes[table_range_key]
546
- new_instance = self.class.find(id, range_key, **keywords)
547
- assign_attributes(new_instance.attributes)
548
- self
549
- end
550
-
551
-
552
- def touch(name=nil)
553
- run_callbacks :touch do
554
- attrs = [:updated_at]
555
- attrs << name if name
556
- t = Time.now
557
- attrs.each { |k| write_attribute name, t }
558
- # TODO: handle lock_version
559
- dynamo_item.attributes.update do |u|
560
- attrs.each do |k|
561
- u.set(k => serialize_attribute(k, t))
562
- end
563
- end
564
- self
565
- end
566
- end
567
-
568
-
569
-
570
- protected
571
-
572
- def evaluate_default(default, type)
573
- return default.call if default.is_a?(Proc)
574
- return "" if default == nil && type == :string
575
- return default.clone if default.is_a?(Array) || default.is_a?(String) # Instances need their own copies
576
- default
577
- end
578
-
579
-
580
- def perform_validations(options={}) # :nodoc:
581
- options[:validate] == false || valid?(options[:context])
582
- end
583
-
584
-
585
- def dynamo_persist
586
- @dynamo_item = dynamo_items.put(serialized_attributes)
587
- @new_record = false
588
- end
589
-
590
-
591
- def post_instantiate(item, consistent)
592
- @dynamo_item = item
593
- @new_record = false
594
- assign_attributes(deserialized_attributes(
595
- hash: nil,
596
- consistent_read: consistent)
597
- )
598
- self
599
- end
600
-
601
- end # Base
602
-
603
- end # Dynamo
604
-