ocean-dynamo 0.1.10 → 0.1.11

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