ocean-dynamo 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +103 -0
- data/Rakefile +34 -0
- data/config/routes.rb +3 -0
- data/lib/ocean-dynamo.rb +9 -0
- data/lib/ocean-dynamo/dynamo.rb +538 -0
- data/lib/ocean-dynamo/engine.rb +6 -0
- data/lib/ocean-dynamo/version.rb +3 -0
- metadata +166 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 339c58baa4132d2d0b311087fbad549938a548b0
|
4
|
+
data.tar.gz: 070fcef8bb7d01c0b7bb1f1151389cae94388b51
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 421fae8d3ff273f4218fa6f4607a6d0538a36ab33490128d716ec8eb1e243333e9590f07a981e426eec8680af4860b2cafc12904b54c0117510c3f0ba4c9a4a8
|
7
|
+
data.tar.gz: 0c7062fab4972a0008cc8e71078d407f4945fcf428a0f0bec36a268ca77ef94618db4abf8bd6bf6bcbf08049b45b60cc1c8fcaf7837bed43eaacdac83e88ae85
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Peter Bengtson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
== ocean-dynamo
|
2
|
+
|
3
|
+
This is the OceanDynamo ruby gem, implementing a highly scalable Amazon DynamoDB
|
4
|
+
near drop-in replacement for ActiveRecord.
|
5
|
+
|
6
|
+
OceanDynamo requires Ruby 2.0 and Ruby on Rails 4.0.0 or later.
|
7
|
+
|
8
|
+
{<img src="https://badge.fury.io/rb/ocean-dynamo.png" alt="Gem Version" />}[http://badge.fury.io/rb/ocean-dynamo]
|
9
|
+
|
10
|
+
|
11
|
+
==== Features
|
12
|
+
|
13
|
+
OceanDynamo will use secondary indices to retrieve related table items,
|
14
|
+
which means it will scale without limits. (NB: this is a pre-release which as yet doesn't
|
15
|
+
implement relations, but they are underway.)
|
16
|
+
|
17
|
+
As one important use case for OceanDynamo is to facilitate the conversion of SQL based
|
18
|
+
ActiveRecord models to DynamoDB based models, it is important that the syntax and semantics
|
19
|
+
of OceanDynamo's operations are as close as possible to those of ActiveRecord. This means
|
20
|
+
that all callbacks should be available, before, around and after, and that they should be
|
21
|
+
called in the same order as in ActiveRecord. OceanDynamo follows this pattern closely and
|
22
|
+
is of course based on ActiveModel.
|
23
|
+
|
24
|
+
The attribute and persistence layer of OceanDynamo is modeled on that of ActiveRecord:
|
25
|
+
there's +save+, +save!+, +create+, +update+, +update!+, +update_attribute+ and all the other
|
26
|
+
methods you're used to. The design goal is always to implement as much as possible of the
|
27
|
+
ActiveRecord interface, without sacrificing scalability. This makes the task of switching from
|
28
|
+
SQL to no-SQL much easier.
|
29
|
+
|
30
|
+
|
31
|
+
=== Documentation
|
32
|
+
|
33
|
+
* Ocean-dynamo gem API: http://rdoc.info/github/OceanDev/ocean-dynamo/frames
|
34
|
+
* Ocean-dynamo gem on Rubygems: https://rubygems.org/gems/ocean-dynamo
|
35
|
+
* Ocean-dynamo source and wiki: https://github.org/OceanDev/ocean-dynamo
|
36
|
+
|
37
|
+
See also Ocean, a Rails framework for creating highly scalable SOAs in the cloud, in which
|
38
|
+
OceanDynamo is used as a central component:
|
39
|
+
* http://wiki.oceanframework.net
|
40
|
+
|
41
|
+
|
42
|
+
=== Running the specs
|
43
|
+
|
44
|
+
To run the specs for the OceanDynamo gem, you must first install the bundle. It will download
|
45
|
+
a gem called +fake_dynamo+, which runs a local, in-memory functional clone of Amazon DynamoDB.
|
46
|
+
We use +fake_dynamo+ during development and testing.
|
47
|
+
|
48
|
+
First of all, copy the AWS configuration file from the template:
|
49
|
+
|
50
|
+
cp spec/dummy/config/aws.yml.example spec/dummy/config/aws.yml
|
51
|
+
|
52
|
+
NB: +aws.yml+ is excluded from source control. This allows you to enter your AWS credentials
|
53
|
+
safely. Note that +aws.yml.example+ is under source control: don't edit it.
|
54
|
+
|
55
|
+
Make sure your have version 0.1.3 of the +fake_dynamo+ gem. It implements the +2011-12-05+ version
|
56
|
+
of the DynamoDB API. We're not yet using the +2012-08-10+ version, as the +aws-sdk+ ruby gem
|
57
|
+
doesn't fully support it. We'll make the change as soon as +aws-sdk+ is updated. Reportedly,
|
58
|
+
it's in the works.
|
59
|
+
|
60
|
+
Next, start +fake_dynamo+:
|
61
|
+
|
62
|
+
fake_dynamo --port 4567
|
63
|
+
|
64
|
+
If this returns errors, make sure that <tt>/usr/local/var/fake_dynamo</tt> exists and
|
65
|
+
is writable:
|
66
|
+
|
67
|
+
sudo mkdir -p /usr/local/var/fake_dynamo
|
68
|
+
sudo chown peterb:staff /usr/local/var/fake_dynamo
|
69
|
+
|
70
|
+
When +fake_dynamo+ runs normally, open another window and issue the following command:
|
71
|
+
|
72
|
+
curl -X DELETE http://localhost:4567
|
73
|
+
|
74
|
+
This will reset the +fake_dynamo+ database. It's not a required operation when starting
|
75
|
+
+fake_dynamo+; we're just using it here as a test that the installation works. It will
|
76
|
+
be issued automatically as part of the test suite, so don't expect test data to survive
|
77
|
+
between runs.
|
78
|
+
|
79
|
+
With +fake_dynamo+ running, you should now be able to do
|
80
|
+
|
81
|
+
rspec
|
82
|
+
|
83
|
+
All tests should pass.
|
84
|
+
|
85
|
+
|
86
|
+
=== Rails console
|
87
|
+
|
88
|
+
The Rails console is available from the built-in dummy application:
|
89
|
+
|
90
|
+
cd spec/dummy
|
91
|
+
rails console
|
92
|
+
|
93
|
+
You may need to initialise the table connection (for each table):
|
94
|
+
|
95
|
+
CloudModel.establish_db_connection
|
96
|
+
|
97
|
+
This will, amongst other things, also create the CloudModel table if it doesn't already
|
98
|
+
exist. On Amazon, this will take a little while. With +fake_dynamo+, it's practically
|
99
|
+
instant.
|
100
|
+
|
101
|
+
When you leave the console, you must navigate back to the top directory (<tt>cd ../..</tt>)
|
102
|
+
in order to be able to run RSpec again.
|
103
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Ocean'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
data/config/routes.rb
ADDED
data/lib/ocean-dynamo.rb
ADDED
@@ -0,0 +1,538 @@
|
|
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 NoPrimaryKeyDeclared < DynamoError; end
|
15
|
+
class UnknownTableStatus < DynamoError; end
|
16
|
+
class UnsupportedType < DynamoError; end
|
17
|
+
class RecordInvalid < DynamoError; end
|
18
|
+
class RecordNotSaved < DynamoError; end
|
19
|
+
class RecordNotFound < DynamoError; end
|
20
|
+
class RecordInConflict < DynamoError; end
|
21
|
+
|
22
|
+
|
23
|
+
class Base
|
24
|
+
|
25
|
+
include ActiveModel::Model
|
26
|
+
include ActiveModel::Validations::Callbacks
|
27
|
+
|
28
|
+
|
29
|
+
# ---------------------------------------------------------
|
30
|
+
#
|
31
|
+
# Class variables and methods
|
32
|
+
#
|
33
|
+
# ---------------------------------------------------------
|
34
|
+
|
35
|
+
class_attribute :dynamo_client, instance_writer: false
|
36
|
+
class_attribute :dynamo_table, instance_writer: false
|
37
|
+
class_attribute :dynamo_items, instance_writer: false
|
38
|
+
|
39
|
+
class_attribute :table_name, instance_writer: false
|
40
|
+
class_attribute :table_name_prefix, instance_writer: false
|
41
|
+
class_attribute :table_name_suffix, instance_writer: false
|
42
|
+
|
43
|
+
class_attribute :table_hash_key, instance_writer: false
|
44
|
+
class_attribute :table_range_key, instance_writer: false
|
45
|
+
|
46
|
+
class_attribute :table_read_capacity_units, instance_writer: false
|
47
|
+
class_attribute :table_write_capacity_units, instance_writer: false
|
48
|
+
|
49
|
+
class_attribute :fields, instance_writer: false
|
50
|
+
|
51
|
+
|
52
|
+
def self.set_table_name(name)
|
53
|
+
self.table_name = name
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.set_table_name_prefix(prefix)
|
57
|
+
self.table_name_prefix = prefix
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.set_table_name_suffix(suffix)
|
61
|
+
self.table_name_suffix = suffix
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def self.compute_table_name
|
66
|
+
name.pluralize.underscore
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def self.table_full_name
|
71
|
+
"#{table_name_prefix}#{table_name}#{table_name_suffix}"
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def self.primary_key(hash_key, range_key=nil)
|
76
|
+
self.table_hash_key = hash_key
|
77
|
+
self.table_range_key = range_key
|
78
|
+
# Find a better place to do the following initialisation:
|
79
|
+
set_table_name compute_table_name unless self.table_name
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def self.read_capacity_units(units)
|
85
|
+
self.table_read_capacity_units = units
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def self.write_capacity_units(units)
|
90
|
+
self.table_write_capacity_units = units
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def self.field(name, type=:string, **pairs)
|
95
|
+
attr_accessor name
|
96
|
+
fields[name] = {type: type,
|
97
|
+
default: pairs[:default]}
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def self.establish_db_connection
|
102
|
+
setup_dynamo
|
103
|
+
if dynamo_table.exists?
|
104
|
+
wait_until_table_is_active
|
105
|
+
else
|
106
|
+
create_table
|
107
|
+
end
|
108
|
+
set_dynamo_table_keys
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def self.setup_dynamo
|
113
|
+
#self.dynamo_client = AWS::DynamoDB::Client.new(:api_version => '2012-08-10')
|
114
|
+
self.dynamo_client ||= AWS::DynamoDB.new
|
115
|
+
self.dynamo_table = dynamo_client.tables[table_full_name]
|
116
|
+
self.dynamo_items = dynamo_table.items
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def self.wait_until_table_is_active
|
121
|
+
loop do
|
122
|
+
case dynamo_table.status
|
123
|
+
when :active
|
124
|
+
set_dynamo_table_keys
|
125
|
+
return
|
126
|
+
when :updating, :creating
|
127
|
+
sleep 1
|
128
|
+
next
|
129
|
+
when :deleting
|
130
|
+
sleep 1 while dynamo_table.exists?
|
131
|
+
create_table
|
132
|
+
return
|
133
|
+
else
|
134
|
+
raise UnknownTableStatus.new("Unknown DynamoDB table status '#{dynamo_table.status}'")
|
135
|
+
end
|
136
|
+
sleep 1
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def self.set_dynamo_table_keys
|
142
|
+
dynamo_table.hash_key = [table_hash_key, fields[table_hash_key][:type]]
|
143
|
+
if table_range_key
|
144
|
+
dynamo_table.range_key = [table_range_key, fields[table_range_key][:type]]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def self.create_table
|
150
|
+
self.dynamo_table = dynamo_client.tables.create(table_full_name,
|
151
|
+
table_read_capacity_units, table_write_capacity_units,
|
152
|
+
hash_key: { table_hash_key => fields[table_hash_key][:type]},
|
153
|
+
range_key: table_range_key && { table_range_key => fields[table_range_key][:type]}
|
154
|
+
)
|
155
|
+
sleep 1 until dynamo_table.status == :active
|
156
|
+
setup_dynamo
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.delete_table
|
161
|
+
return false unless dynamo_table.exists? && dynamo_table.status == :active
|
162
|
+
dynamo_table.delete
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def self.create(attributes = nil, &block)
|
168
|
+
object = new(attributes)
|
169
|
+
yield(object) if block_given?
|
170
|
+
object.save
|
171
|
+
object
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
def self.create!(attributes = nil, &block)
|
176
|
+
object = new(attributes)
|
177
|
+
yield(object) if block_given?
|
178
|
+
object.save!
|
179
|
+
object
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
def self.find(hash, range=nil, consistent: false)
|
184
|
+
item = dynamo_items[hash, range]
|
185
|
+
raise RecordNotFound unless item.exists?
|
186
|
+
new.send(:post_instantiate, item, consistent)
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
def self.delete(hash, range=nil)
|
191
|
+
item = dynamo_items[hash, range]
|
192
|
+
return false unless item.exists?
|
193
|
+
item.delete
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
def self.count
|
199
|
+
dynamo_table.item_count || -1 # The || -1 is for fake_dynamo specs.
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
# ---------------------------------------------------------
|
204
|
+
#
|
205
|
+
# Callbacks
|
206
|
+
#
|
207
|
+
# ---------------------------------------------------------
|
208
|
+
|
209
|
+
define_model_callbacks :initialize, only: :after
|
210
|
+
define_model_callbacks :save
|
211
|
+
define_model_callbacks :create
|
212
|
+
define_model_callbacks :update
|
213
|
+
define_model_callbacks :destroy
|
214
|
+
define_model_callbacks :commit, only: :after
|
215
|
+
define_model_callbacks :touch
|
216
|
+
|
217
|
+
|
218
|
+
# ---------------------------------------------------------
|
219
|
+
#
|
220
|
+
# Class initialisation, done once at load time
|
221
|
+
#
|
222
|
+
# ---------------------------------------------------------
|
223
|
+
|
224
|
+
self.table_read_capacity_units = 10
|
225
|
+
self.table_write_capacity_units = 5
|
226
|
+
|
227
|
+
self.fields = HashWithIndifferentAccess.new
|
228
|
+
DEFAULT_FIELDS.each { |k, name, **pairs| Base.field k, name, **pairs }
|
229
|
+
|
230
|
+
|
231
|
+
# ---------------------------------------------------------
|
232
|
+
#
|
233
|
+
# Instance variables and methods
|
234
|
+
#
|
235
|
+
# ---------------------------------------------------------
|
236
|
+
|
237
|
+
attr_reader :attributes
|
238
|
+
attr_reader :destroyed
|
239
|
+
attr_reader :new_record
|
240
|
+
attr_reader :dynamo_item
|
241
|
+
|
242
|
+
|
243
|
+
def initialize(attributes={})
|
244
|
+
run_callbacks :initialize do
|
245
|
+
@attributes = HashWithIndifferentAccess.new
|
246
|
+
fields.each do |name, v|
|
247
|
+
write_attribute(name, evaluate_default(v[:default])) unless read_attribute(name)
|
248
|
+
self.class.class_eval "def #{name}; read_attribute('#{name}'); end"
|
249
|
+
self.class.class_eval "def #{name}=(value); write_attribute('#{name}', value); end"
|
250
|
+
if fields[name][:type] == :boolean
|
251
|
+
self.class.class_eval "def #{name}?; read_attribute('#{name}'); end"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
super
|
255
|
+
@dynamo_item = nil
|
256
|
+
@destroyed = false
|
257
|
+
@new_record = true
|
258
|
+
raise NoPrimaryKeyDeclared unless table_hash_key
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
def read_attribute(name)
|
264
|
+
@attributes[name]
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
def write_attribute(name, value)
|
269
|
+
@attributes[name] = value
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
def [](attribute)
|
274
|
+
read_attribute attribute
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
def []=(attribute, value)
|
279
|
+
write_attribute attribute, value
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
def id
|
284
|
+
read_attribute(table_hash_key)
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
def id=(value)
|
289
|
+
write_attribute(table_hash_key, value)
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
def to_key
|
294
|
+
return nil unless persisted?
|
295
|
+
key = respond_to?(:id) && id
|
296
|
+
key ? [key] : nil
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
|
301
|
+
def assign_attributes(values)
|
302
|
+
values.each do |k, v|
|
303
|
+
send("#{k}=", v)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
def serialized_attributes
|
309
|
+
result = {}
|
310
|
+
fields.each do |attribute, metadata|
|
311
|
+
serialized = serialize_attribute(attribute, read_attribute(attribute), metadata)
|
312
|
+
result[attribute] = serialized unless serialized == nil
|
313
|
+
end
|
314
|
+
result
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
def serialize_attribute(attribute, value,
|
319
|
+
metadata=fields[attribute],
|
320
|
+
type: metadata[:type],
|
321
|
+
default: metadata[:default])
|
322
|
+
return nil if value == nil
|
323
|
+
case type
|
324
|
+
when :string
|
325
|
+
value == "" ? nil : value
|
326
|
+
when :integer
|
327
|
+
value
|
328
|
+
when :float
|
329
|
+
value
|
330
|
+
when :boolean
|
331
|
+
value ? "true" : "false"
|
332
|
+
when :datetime
|
333
|
+
value.to_i
|
334
|
+
when :serialized
|
335
|
+
value.to_json
|
336
|
+
else
|
337
|
+
raise UnsupportedType.new(type.to_s)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
def deserialized_attributes(consistent_read: false, hash: nil)
|
343
|
+
hash ||= dynamo_item.attributes.to_hash(consistent_read: consistent_read)
|
344
|
+
result = {}
|
345
|
+
fields.each do |attribute, metadata|
|
346
|
+
result[attribute] = deserialize_attribute(hash[attribute], metadata)
|
347
|
+
end
|
348
|
+
result
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
def deserialize_attribute(value, metadata,
|
353
|
+
type: metadata[:type],
|
354
|
+
default: metadata[:default])
|
355
|
+
if value == nil && default != nil && type != :string
|
356
|
+
return evaluate_default(default)
|
357
|
+
end
|
358
|
+
case type
|
359
|
+
when :string
|
360
|
+
return "" if value == nil
|
361
|
+
value
|
362
|
+
when :integer
|
363
|
+
return nil if value == nil
|
364
|
+
value.is_a?(Array) ? value.collect(&:to_i) : value.to_i
|
365
|
+
when :float
|
366
|
+
return nil if value == nil
|
367
|
+
value.is_a?(Array) ? value.collect(&:to_f) : value.to_f
|
368
|
+
when :boolean
|
369
|
+
case value
|
370
|
+
when "true"
|
371
|
+
true
|
372
|
+
when "false"
|
373
|
+
false
|
374
|
+
else
|
375
|
+
nil
|
376
|
+
end
|
377
|
+
when :datetime
|
378
|
+
return nil if value == nil
|
379
|
+
Time.at(value.to_i)
|
380
|
+
when :serialized
|
381
|
+
return nil if value == nil
|
382
|
+
JSON.parse(value)
|
383
|
+
else
|
384
|
+
raise UnsupportedType.new(type.to_s)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
|
389
|
+
def destroyed?
|
390
|
+
@destroyed
|
391
|
+
end
|
392
|
+
|
393
|
+
|
394
|
+
def new_record?
|
395
|
+
@new_record
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
def persisted?
|
400
|
+
!(new_record? || destroyed?)
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
def save
|
405
|
+
begin
|
406
|
+
create_or_update
|
407
|
+
rescue RecordInvalid
|
408
|
+
false
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
def save!(*)
|
414
|
+
create_or_update || raise(RecordNotSaved)
|
415
|
+
end
|
416
|
+
|
417
|
+
|
418
|
+
def update_attributes(attributes={})
|
419
|
+
assign_attributes(attributes)
|
420
|
+
save
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
def update_attributes!(attributes={})
|
425
|
+
assign_attributes(attributes)
|
426
|
+
save!
|
427
|
+
end
|
428
|
+
|
429
|
+
|
430
|
+
def create_or_update
|
431
|
+
result = new_record? ? create : update
|
432
|
+
result != false
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
def create
|
437
|
+
return false unless valid?(:create)
|
438
|
+
run_callbacks :commit do
|
439
|
+
run_callbacks :save do
|
440
|
+
run_callbacks :create do
|
441
|
+
write_attribute(table_hash_key, SecureRandom.uuid) if read_attribute(table_hash_key) == nil
|
442
|
+
t = Time.now
|
443
|
+
self.created_at ||= t
|
444
|
+
self.updated_at ||= t
|
445
|
+
dynamo_persist
|
446
|
+
true
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
|
453
|
+
def update
|
454
|
+
return false unless valid?(:update)
|
455
|
+
run_callbacks :commit do
|
456
|
+
run_callbacks :save do
|
457
|
+
run_callbacks :update do
|
458
|
+
self.updated_at = Time.now
|
459
|
+
dynamo_persist
|
460
|
+
true
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def destroy
|
467
|
+
run_callbacks :commit do
|
468
|
+
run_callbacks :destroy do
|
469
|
+
delete
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
def delete
|
476
|
+
if persisted?
|
477
|
+
@dynamo_item.delete
|
478
|
+
end
|
479
|
+
@destroyed = true
|
480
|
+
freeze
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
def reload(**keywords)
|
485
|
+
range_key = table_range_key && attributes[table_range_key]
|
486
|
+
new_instance = self.class.find(id, range_key, **keywords)
|
487
|
+
assign_attributes(new_instance.attributes)
|
488
|
+
self
|
489
|
+
end
|
490
|
+
|
491
|
+
|
492
|
+
def touch(name=nil)
|
493
|
+
run_callbacks :touch do
|
494
|
+
attrs = [:updated_at]
|
495
|
+
attrs << name if name
|
496
|
+
t = Time.now
|
497
|
+
attrs.each { |k| write_attribute name, t }
|
498
|
+
# TODO: handle lock_version
|
499
|
+
dynamo_item.attributes.update do |u|
|
500
|
+
attrs.each do |k|
|
501
|
+
u.set(k => serialize_attribute(k, t))
|
502
|
+
end
|
503
|
+
end
|
504
|
+
self
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
|
510
|
+
protected
|
511
|
+
|
512
|
+
def evaluate_default(v)
|
513
|
+
return v.call if v.is_a?(Proc)
|
514
|
+
return v.clone if v.is_a?(Array) || v.is_a?(String) # Instances need their own copy
|
515
|
+
v
|
516
|
+
end
|
517
|
+
|
518
|
+
|
519
|
+
def dynamo_persist
|
520
|
+
@dynamo_item = dynamo_items.put(serialized_attributes)
|
521
|
+
@new_record = false
|
522
|
+
end
|
523
|
+
|
524
|
+
|
525
|
+
def post_instantiate(item, consistent)
|
526
|
+
@dynamo_item = item
|
527
|
+
@new_record = false
|
528
|
+
assign_attributes(deserialized_attributes(
|
529
|
+
hash: nil,
|
530
|
+
consistent_read: consistent)
|
531
|
+
)
|
532
|
+
self
|
533
|
+
end
|
534
|
+
|
535
|
+
end # Base
|
536
|
+
|
537
|
+
end # Dynamo
|
538
|
+
|
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ocean-dynamo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Bengtson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: factory_girl_rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fake_dynamo
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.1.3
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.1.3
|
111
|
+
description: "== ocean-dynamo\n\nThis is the OceanDynamo ruby gem, implementing a
|
112
|
+
highly scalable Amazon DynamoDB near drop-in \nreplacement for ActiveRecord.\n\nOceanDynamo
|
113
|
+
will use secondary indices to retrieve related table items, \nwhich means it will
|
114
|
+
scale without limits. (NB: this is a pre-release which as yet doesn't\nimplement
|
115
|
+
relations, but they are underway.)\n\nAs one important use case for OceanDynamo
|
116
|
+
is to facilitate the conversion of SQL based\nActiveRecord models to DynamoDB based
|
117
|
+
models, it is important that the syntax and semantics\nof OceanDynamo's operations
|
118
|
+
are as close as possible to those of ActiveRecord. This means\nthat all callbacks
|
119
|
+
should be available, before, around and after, and that they should be\ncalled in
|
120
|
+
the same order as in ActiveRecord. Ocean-dynamo follows this pattern closely and\nis
|
121
|
+
of course based on ActiveModel.\n\nThe attribute and persistence layer of OceanDynamo
|
122
|
+
is modeled on that of ActiveRecord:\nthere's +save+, +save!+, +create+, +update+,
|
123
|
+
+update!+, +update_attribute+ and all the other\nmethods you're used to. The design
|
124
|
+
goal is always to implement as much as possible of the\nActiveRecord interface,
|
125
|
+
without sacrificing scalability. This makes the task of switching from\nSQL to no-SQL
|
126
|
+
much easier.\n\nSee also Ocean, a Rails framework for creating highly scalable SOAs
|
127
|
+
in the cloud, in which\nocean-dynamo is used as a central component: http://wiki.oceanframework.net"
|
128
|
+
email:
|
129
|
+
- peter@peterbengtson.com
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- config/routes.rb
|
135
|
+
- lib/ocean-dynamo/dynamo.rb
|
136
|
+
- lib/ocean-dynamo/engine.rb
|
137
|
+
- lib/ocean-dynamo/version.rb
|
138
|
+
- lib/ocean-dynamo.rb
|
139
|
+
- MIT-LICENSE
|
140
|
+
- Rakefile
|
141
|
+
- README.rdoc
|
142
|
+
homepage: https://github.com/OceanDev/ocean-dynamo
|
143
|
+
licenses:
|
144
|
+
- MIT
|
145
|
+
metadata: {}
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: 2.0.0
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
requirements: []
|
161
|
+
rubyforge_project:
|
162
|
+
rubygems_version: 2.0.7
|
163
|
+
signing_key:
|
164
|
+
specification_version: 4
|
165
|
+
summary: This gem implements common Ocean behaviour for Ruby and Ruby on Rails.
|
166
|
+
test_files: []
|