lotus-dynamodb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,211 @@
1
+ require 'lotus/utils/kernel'
2
+ require 'multi_json'
3
+
4
+ module Lotus
5
+ module Model
6
+ module Adapters
7
+ module Dynamodb
8
+ # Translates values from/to the database with the corresponding Ruby type.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ class Coercer < Lotus::Model::Mapping::Coercer
13
+ SKIPPED_KLASSES = [Float, Integer, Set, String]
14
+ SUPPORTED_KLASSES = [AWS::DynamoDB::Binary, Array, Boolean, Date, DateTime, Hash, Time]
15
+
16
+ # Converts value from given type to DynamoDB record value.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ def from_aws_dynamodb_binary(value)
21
+ return value if value.nil? || value.is_a?(AWS::DynamoDB::Binary)
22
+ AWS::DynamoDB::Binary.new(value)
23
+ end
24
+
25
+ alias_method :to_aws_dynamodb_binary, :from_aws_dynamodb_binary
26
+
27
+ # Converts value from given type to DynamoDB record value.
28
+ #
29
+ # @api private
30
+ # @since 0.1.0
31
+ def from_array(value)
32
+ _serialize(value)
33
+ end
34
+
35
+ # Converts value from DynamoDB record value to given type.
36
+ #
37
+ # @api private
38
+ # @since 0.1.0
39
+ def to_array(value)
40
+ _deserialize(value)
41
+ end
42
+
43
+ # Converts value from given type to DynamoDB record value.
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ def from_boolean(value)
48
+ value ? 1 : 0
49
+ end
50
+
51
+ # Converts value from DynamoDB record value to given type.
52
+ #
53
+ # @api private
54
+ # @since 0.1.0
55
+ def to_boolean(value)
56
+ value.to_i == 1
57
+ end
58
+
59
+ # Converts value from given type to DynamoDB record value.
60
+ #
61
+ # @api private
62
+ # @since 0.1.0
63
+ def from_date(value)
64
+ value.to_time.to_i
65
+ end
66
+
67
+ # Converts value from DynamoDB record value to given type.
68
+ #
69
+ # @api private
70
+ # @since 0.1.0
71
+ def to_date(value)
72
+ Time.at(value.to_i).to_date
73
+ end
74
+
75
+ # Converts value from given type to DynamoDB record value.
76
+ #
77
+ # @api private
78
+ # @since 0.1.0
79
+ def from_datetime(value)
80
+ value.to_time.to_f
81
+ end
82
+
83
+ # Converts value from DynamoDB record value to given type.
84
+ #
85
+ # @api private
86
+ # @since 0.1.0
87
+ def to_datetime(value)
88
+ Time.at(value.to_f).to_datetime
89
+ end
90
+
91
+ # Converts value from given type to DynamoDB record value.
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ def from_hash(value)
96
+ _serialize(value)
97
+ end
98
+
99
+ # Converts value from DynamoDB record value to given type.
100
+ #
101
+ # @api private
102
+ # @since 0.1.0
103
+ def to_hash(value)
104
+ _deserialize(value)
105
+ end
106
+
107
+ # Converts value from given type to DynamoDB record value.
108
+ #
109
+ # @api private
110
+ # @since 0.1.0
111
+ def from_time(value)
112
+ value.to_f
113
+ end
114
+
115
+ # Converts value from DynamoDB record value to given type.
116
+ #
117
+ # @api private
118
+ # @since 0.1.0
119
+ def to_time(value)
120
+ Time.at(value.to_f)
121
+ end
122
+
123
+ private
124
+ # Compile itself for performance boost.
125
+ #
126
+ # @api private
127
+ # @since 0.1.0
128
+ def _compile!
129
+ instance_eval(SKIPPED_KLASSES.map do |klass|
130
+ %{
131
+ def from_#{_method_name(klass)}(value)
132
+ value
133
+ end
134
+
135
+ def to_#{_method_name(klass)}(value)
136
+ value
137
+ end
138
+ }
139
+ end.join("\n"))
140
+
141
+ code = @collection.attributes.map do |_,(klass,mapped)|
142
+ %{
143
+ def deserialize_#{ mapped }(value)
144
+ #{kernel_wrap(klass) { "from_#{_method_name(klass)}(value)" }}
145
+ end
146
+
147
+ def serialize_#{ mapped }(value)
148
+ from_#{_method_name(klass)}(value)
149
+ end
150
+ }
151
+ end.join("\n")
152
+
153
+ instance_eval %{
154
+ def to_record(entity)
155
+ if entity.id
156
+ Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{mapped},from_#{_method_name(klass)}(entity.#{name})"}.join(',') }]]
157
+ else
158
+ Hash[*[#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,(klass,mapped)| ":#{mapped},from_#{_method_name(klass)}(entity.#{name})"}.join(',') }]]
159
+ end
160
+ end
161
+
162
+ def from_record(record)
163
+ #{ @collection.entity }.new(
164
+ Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{name},#{kernel_wrap(klass) { "to_#{_method_name(klass)}(record[:#{mapped}])" }}"}.join(',') }]]
165
+ )
166
+ end
167
+
168
+ #{ code }
169
+ }
170
+ end
171
+
172
+ # Wraps string in Lotus::Utils::Kernel call if needed.
173
+ #
174
+ # @api private
175
+ # @since 0.1.0
176
+ def kernel_wrap(klass)
177
+ if klass.to_s.include?("::")
178
+ yield
179
+ else
180
+ "Lotus::Utils::Kernel.#{klass}(#{yield})"
181
+ end
182
+ end
183
+
184
+ # Returns method name from klass.
185
+ #
186
+ # @api private
187
+ # @since 0.1.0
188
+ def _method_name(klass)
189
+ klass.to_s.downcase.gsub("::", "_")
190
+ end
191
+
192
+ # Serializes value to string.
193
+ #
194
+ # @api private
195
+ # @since 0.1.0
196
+ def _serialize(value)
197
+ MultiJson.dump(value)
198
+ end
199
+
200
+ # Deserializes value from string.
201
+ #
202
+ # @api private
203
+ # @since 0.1.0
204
+ def _deserialize(value)
205
+ MultiJson.load(value)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,321 @@
1
+ require 'securerandom'
2
+ require 'aws-sdk'
3
+ require 'lotus/utils/hash'
4
+
5
+ module Lotus
6
+ module Model
7
+ module Adapters
8
+ module Dynamodb
9
+ # Acts like table, using AWS::DynamoDB::Client.
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ class Collection
14
+ include AWS::DynamoDB::Types
15
+
16
+ # Response interface provides count and entities.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ class Response
21
+ attr_accessor :count, :entities
22
+
23
+ def initialize(count, entities = nil)
24
+ @count, @entities = count, entities
25
+ end
26
+ end
27
+
28
+ # @attr_reader name [String] the name of the collection (eg. `users`)
29
+ #
30
+ # @since 0.1.0
31
+ # @api private
32
+ attr_reader :name
33
+
34
+ # @attr_reader identity [Symbol] the primary key of the collection
35
+ # (eg. `:id`)
36
+ #
37
+ # @since 0.1.0
38
+ # @api private
39
+ attr_reader :identity
40
+
41
+ # Initialize a collection.
42
+ #
43
+ # @param client [AWS::DynamoDB::Client] DynamoDB client
44
+ # @param coercer [Lotus::Model::Adapters::Dynamodb::Coercer]
45
+ # @param name [Symbol] the name of the collection (eg. `:users`)
46
+ # @param identity [Symbol] the primary key of the collection
47
+ # (eg. `:id`).
48
+ #
49
+ # @api private
50
+ # @since 0.1.0
51
+ def initialize(client, coercer, name, identity)
52
+ @client, @coercer = client, coercer
53
+ @name, @identity = name.to_s, identity
54
+ @key_schema = {}
55
+ end
56
+
57
+ # Creates a record for the given entity and returns a primary key.
58
+ #
59
+ # @param entity [Object] the entity to persist
60
+ #
61
+ # @see Lotus::Model::Adapters::Dynamodb::Command#create
62
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#put_item-instance_method
63
+ #
64
+ # @return the primary key of the just created record.
65
+ #
66
+ # @api private
67
+ # @since 0.1.0
68
+ def create(entity)
69
+ entity[identity] ||= SecureRandom.uuid
70
+
71
+ @client.put_item(
72
+ table_name: name,
73
+ item: serialize_item(entity),
74
+ )
75
+
76
+ entity[identity]
77
+ end
78
+
79
+ # Updates the record corresponding to the given entity.
80
+ #
81
+ # @param entity [Object] the entity to persist
82
+ #
83
+ # @see Lotus::Model::Adapters::Dynamodb::Command#update
84
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#update_item-instance_method
85
+ #
86
+ # @api private
87
+ # @since 0.1.0
88
+ def update(entity)
89
+ @client.update_item(
90
+ table_name: name,
91
+ key: serialize_key(entity),
92
+ attribute_updates: serialize_attributes(entity),
93
+ )
94
+ end
95
+
96
+ # Deletes the record corresponding to the given entity.
97
+ #
98
+ # @param entity [Object] the entity to delete
99
+ #
100
+ # @see Lotus::Model::Adapters::Dynamodb::Command#delete
101
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#delete_item-instance_method
102
+ #
103
+ # @api private
104
+ # @since 0.1.0
105
+ def delete(entity)
106
+ @client.delete_item(
107
+ table_name: name,
108
+ key: serialize_key(entity),
109
+ )
110
+ end
111
+
112
+ # Returns an unique record from the given collection, with the given
113
+ # id.
114
+ #
115
+ # @param key [Array] the identity of the object
116
+ #
117
+ # @see Lotus::Model::Adapters::Dynamodb::Command#get
118
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#get_item-instance_method
119
+ #
120
+ # @return [Hash] the serialized record
121
+ #
122
+ # @api private
123
+ # @since 0.1.0
124
+ def get(key)
125
+ return if key.any? { |v| v.to_s == "" }
126
+ return if key.count != key_schema.count
127
+
128
+ response = @client.get_item(
129
+ table_name: name,
130
+ key: serialize_key(key),
131
+ )
132
+
133
+ deserialize_item(response[:item]) if response[:item]
134
+ end
135
+
136
+ # Performs DynamoDB query operation.
137
+ #
138
+ # @param options [Hash] AWS::DynamoDB::Client options
139
+ #
140
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#query-instance_method
141
+ #
142
+ # @return [Array<Hash>] the serialized entities
143
+ #
144
+ # @api private
145
+ # @since 0.1.0
146
+ def query(options = {})
147
+ response = @client.query(options.merge(table_name: name))
148
+ deserialize_response(response)
149
+ end
150
+
151
+ # Performs DynamoDB scan operation.
152
+ #
153
+ # @param options [Hash] AWS::DynamoDB::Client options
154
+ #
155
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#scan-instance_method
156
+ #
157
+ # @return [Array<Hash>] the serialized entities
158
+ #
159
+ # @api private
160
+ # @since 0.1.0
161
+ def scan(options = {})
162
+ response = @client.scan(options.merge(table_name: name))
163
+ deserialize_response(response)
164
+ end
165
+
166
+ # Fetches DynamoDB table schema.
167
+ #
168
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#describe_table-instance_method
169
+ #
170
+ # @return [Hash] table schema definition
171
+ #
172
+ # @api private
173
+ # @since 0.1.0
174
+ def schema
175
+ @schema ||= @client.describe_table(table_name: name).fetch(:table)
176
+ end
177
+
178
+ # Maps table key schema to hash with attribute name as key and key
179
+ # type as value.
180
+ #
181
+ # @param index [String] index to check (defaults to table itself)
182
+ #
183
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#schema
184
+ #
185
+ # @return [Hash] key schema definition
186
+ #
187
+ # @api private
188
+ # @since 0.1.0
189
+ def key_schema(index = nil)
190
+ return @key_schema[index] if @key_schema[index]
191
+
192
+ current_schema = if index
193
+ everything = Array(schema[:local_secondary_indexes]) +
194
+ Array(schema[:global_secondary_indexes])
195
+ indexes = Hash[everything.map { |i| [i[:index_name], i] }]
196
+ indexes[index][:key_schema]
197
+ else
198
+ schema[:key_schema]
199
+ end
200
+
201
+ @key_schema[index] ||= Hash[current_schema.to_a.map do |key|
202
+ [key[:attribute_name].to_sym, key[:key_type]]
203
+ end]
204
+ end
205
+
206
+ # Checks if given column is in key schema or not.
207
+ #
208
+ # @param column [String] column to check
209
+ # @param index [String] index to check (defaults to table itself)
210
+ #
211
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#key_schema
212
+ #
213
+ # @return [Boolean]
214
+ #
215
+ # @api private
216
+ # @since 0.1.0
217
+ def key?(column, index = nil)
218
+ key_schema(index).has_key?(column)
219
+ end
220
+
221
+ # Coerce and format attribute value to match DynamoDB type.
222
+ #
223
+ # @param column [String] the attribute column
224
+ # @param value [Object] the attribute value
225
+ #
226
+ # @see AWS::DynamoDB::Types
227
+ #
228
+ # @return [Hash] the formatted attribute
229
+ #
230
+ # @api private
231
+ # @since 0.1.0
232
+ def format_attribute(column, value)
233
+ value = @coercer.public_send(:"serialize_#{ column }", value)
234
+ format_attribute_value(value)
235
+ end
236
+
237
+ # Serialize given record to have proper attributes for 'item' query.
238
+ #
239
+ # @param record [Hash] the serialized record
240
+ #
241
+ # @see AWS::DynamoDB::Types
242
+ #
243
+ # @return [Hash] the serialized item
244
+ #
245
+ # @api private
246
+ # @since 0.1.0
247
+ def serialize_item(record)
248
+ Hash[record.map { |k, v| [k.to_s, format_attribute_value(v)] }]
249
+ end
250
+
251
+ # Serialize given record or primary key to have proper attributes
252
+ # for 'key' query.
253
+ #
254
+ # @param record [Hash,Array] the serialized record or primary key
255
+ #
256
+ # @see AWS::DynamoDB::Types
257
+ #
258
+ # @return [Hash] the serialized key
259
+ #
260
+ # @api private
261
+ # @since 0.1.0
262
+ def serialize_key(record)
263
+ Hash[key_schema.keys.each_with_index.map do |k, idx|
264
+ v = record.is_a?(Hash) ? record[k] : record[idx]
265
+ [k.to_s, format_attribute(k, v)]
266
+ end]
267
+ end
268
+
269
+ # Serialize given entity to exclude key schema attributes.
270
+ #
271
+ # @param entity [Hash] the entity
272
+ #
273
+ # @see AWS::DynamoDB::Types
274
+ #
275
+ # @return [Hash] the serialized attributes
276
+ #
277
+ # @api private
278
+ # @since 0.1.0
279
+ def serialize_attributes(entity)
280
+ keys = key_schema.keys
281
+ Hash[entity.reject { |k, _| keys.include?(k) }.map do |k, v|
282
+ [k.to_s, { value: format_attribute_value(v), action: "PUT" }]
283
+ end]
284
+ end
285
+
286
+ # Deserialize DynamoDB scan/query response.
287
+ #
288
+ # @param response [Hash] the serialized response
289
+ #
290
+ # @return [Hash] the deserialized response
291
+ #
292
+ # @api private
293
+ # @since 0.1.0
294
+ def deserialize_response(response)
295
+ result = Response.new(response[:count])
296
+
297
+ result.entities = response[:member].map do |item|
298
+ deserialize_item(item)
299
+ end if response[:member]
300
+
301
+ result
302
+ end
303
+
304
+ # Deserialize item from DynamoDB response.
305
+ #
306
+ # @param item [Hash] the serialized item
307
+ #
308
+ # @see AWS::DynamoDB::Types
309
+ #
310
+ # @return [Hash] the deserialized record
311
+ #
312
+ # @api private
313
+ # @since 0.1.0
314
+ def deserialize_item(record)
315
+ Lotus::Utils::Hash.new(values_from_response_hash(record)).symbolize!
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,117 @@
1
+ module Lotus
2
+ module Model
3
+ module Adapters
4
+ module Dynamodb
5
+ # Execute a command for the given collection.
6
+ #
7
+ # @see Lotus::Model::Adapters::Dynamodb::Collection
8
+ # @see Lotus::Model::Mapping::Collection
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ class Command
13
+ # Initialize a command.
14
+ #
15
+ # @param dataset [Lotus::Model::Adapters::Dynamodb::Collection]
16
+ # @param collection [Lotus::Model::Mapping::Collection]
17
+ #
18
+ # @return [Lotus::Model::Adapters::Dynamodb::Command]
19
+ #
20
+ # @api private
21
+ # @since 0.1.0
22
+ def initialize(dataset, collection)
23
+ @dataset, @collection = dataset, collection
24
+ end
25
+
26
+ # Creates a record for the given entity.
27
+ #
28
+ # @param entity [Object] the entity to persist
29
+ #
30
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#create
31
+ #
32
+ # @return the primary key of the just created record.
33
+ #
34
+ # @api private
35
+ # @since 0.1.0
36
+ def create(entity)
37
+ @dataset.create(
38
+ _serialize(entity)
39
+ )
40
+ end
41
+
42
+ # Updates the corresponding record for the given entity.
43
+ #
44
+ # @param entity [Object] the entity to persist
45
+ #
46
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#update
47
+ #
48
+ # @api private
49
+ # @since 0.1.0
50
+ def update(entity)
51
+ @dataset.update(
52
+ _serialize(entity)
53
+ )
54
+ end
55
+
56
+ # Deletes the corresponding record for the given entity.
57
+ #
58
+ # @param entity [Object] the entity to delete
59
+ #
60
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#delete
61
+ #
62
+ # @api private
63
+ # @since 0.1.0
64
+ def delete(entity)
65
+ @dataset.delete(
66
+ _serialize(entity)
67
+ )
68
+ end
69
+
70
+ # Returns an unique record from the given collection, with the given
71
+ # id.
72
+ #
73
+ # @param key [Array] the identity of the object
74
+ #
75
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#get
76
+ #
77
+ # @return [Object] the entity
78
+ #
79
+ # @api private
80
+ # @since 0.1.0
81
+ def get(key)
82
+ @collection.deserialize(
83
+ [@dataset.get(key)].compact
84
+ ).first
85
+ end
86
+
87
+ # Deletes all the records from the table.
88
+ #
89
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#scan
90
+ # @see Lotus::Model::Mapping::Collection
91
+ #
92
+ # @api private
93
+ # @since 0.1.0
94
+ def clear
95
+ @collection.deserialize(@dataset.scan.entities).each do |entity|
96
+ delete(entity)
97
+ end
98
+ end
99
+
100
+ private
101
+ # Serialize the given entity before to persist in the database.
102
+ #
103
+ # @param entity [Object] the entity
104
+ #
105
+ # @return [Hash] the serialized entity
106
+ #
107
+ # @api private
108
+ # @since 0.1.0
109
+ def _serialize(entity)
110
+ serialized = @collection.serialize(entity)
111
+ serialized.delete_if { |_, v| v.nil? }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end