s3db 0.0.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/Gemfile +10 -0
- data/Gemfile.lock +74 -0
- data/bin/s3db +4 -0
- data/lib/s3db.rb +16 -0
- data/lib/s3db/backend.rb +4 -0
- data/lib/s3db/collection.rb +70 -0
- data/lib/s3db/database.rb +100 -0
- data/lib/s3db/file_backend.rb +492 -0
- data/lib/s3db/record.rb +195 -0
- data/lib/s3db/utils.rb +11 -0
- data/s3db.gemspec +40 -0
- data/spec/lib/s3db/collection_spec.rb +91 -0
- data/spec/lib/s3db/database_spec.rb +141 -0
- data/spec/lib/s3db/file_backend_spec.rb +612 -0
- data/spec/lib/s3db/record_spec.rb +286 -0
- data/spec/lib/s3db/utils_spec.rb +27 -0
- data/spec/spec_helper.rb +110 -0
- metadata +169 -0
data/lib/s3db/record.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
module S3DB
|
2
|
+
module Record
|
3
|
+
attr_accessor :data
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
attr_accessor :_database, :_collection, :_schema, :_id_generator, :_id_field
|
7
|
+
|
8
|
+
# Set the database to use for the record.
|
9
|
+
#
|
10
|
+
# database - S3DB::Database instance. Required.
|
11
|
+
#
|
12
|
+
# Returns the database instance.
|
13
|
+
def database(database)
|
14
|
+
self._database = database
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the collection to a new collection with the passed name.
|
18
|
+
#
|
19
|
+
# name - String name of the collection. Required.
|
20
|
+
#
|
21
|
+
# returns the newly created S3DB::Collection.
|
22
|
+
def collection_name(name)
|
23
|
+
self._collection = S3DB::Collection.create(_database, name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the id generator for new records.
|
27
|
+
#
|
28
|
+
# proc - Proc to call, returning the id field. Required.
|
29
|
+
#
|
30
|
+
# returns the Proc instance.
|
31
|
+
def id_generator(proc)
|
32
|
+
self._id_generator = proc
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set the field to use for the id. It will be passed to the id_generator
|
36
|
+
# proc.
|
37
|
+
#
|
38
|
+
# id_field - String name of field. Required.
|
39
|
+
#
|
40
|
+
# returns the id_field.
|
41
|
+
def id_field(key)
|
42
|
+
self._id_field = key.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set a field on the record to validate as a string.
|
46
|
+
#
|
47
|
+
# key - String name of key. Required.
|
48
|
+
#
|
49
|
+
# returns the schema.
|
50
|
+
def string(key)
|
51
|
+
schema = instance_variable_get(:@_schema) || {}
|
52
|
+
schema[key.to_s] = 'String'
|
53
|
+
instance_variable_set(:@_schema, schema)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Locate all records in the collection.
|
57
|
+
#
|
58
|
+
# returns the output from S3DB::Collection#list_records.
|
59
|
+
def all
|
60
|
+
instance_variable_get(:@_collection).list_records.map do |rec|
|
61
|
+
new(JSON.parse(rec))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Locate a single record from the collection by filename.
|
66
|
+
#
|
67
|
+
# filename - String filename to return. Required.
|
68
|
+
#
|
69
|
+
# returns the record on success; raises an error on failure.
|
70
|
+
def find(filename)
|
71
|
+
record = new(_id: filename)
|
72
|
+
res = _database.backend.read_record(_database.name, _collection.name, record.__send__(:_filename))
|
73
|
+
|
74
|
+
raise ArgumentError, 'missing record!' unless res
|
75
|
+
|
76
|
+
new(JSON.parse(res))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a new record, and write it to disk.
|
80
|
+
#
|
81
|
+
# data - Hash data for the record.
|
82
|
+
#
|
83
|
+
# returns an instance of the record.
|
84
|
+
def create(data)
|
85
|
+
record = new(data)
|
86
|
+
record.__send__(:_set_id)
|
87
|
+
record.save
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Instantiate a new record.
|
92
|
+
#
|
93
|
+
# data - Hash of data. Required.
|
94
|
+
#
|
95
|
+
# returns a new instance of the record.
|
96
|
+
def initialize(data)
|
97
|
+
hash = {}
|
98
|
+
data.each_pair do |k,v|
|
99
|
+
hash[k.to_s] = v
|
100
|
+
end
|
101
|
+
|
102
|
+
@data = hash
|
103
|
+
end
|
104
|
+
|
105
|
+
def new_record?
|
106
|
+
_id.nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Save an instantiated record.
|
110
|
+
#
|
111
|
+
# returns the record on success or failure.
|
112
|
+
def save
|
113
|
+
_set_id
|
114
|
+
|
115
|
+
return false if _id.nil?
|
116
|
+
|
117
|
+
self.class._database.backend.write_record(
|
118
|
+
self.class._database.name,
|
119
|
+
self.class._collection.name,
|
120
|
+
_filename,
|
121
|
+
@data.to_json
|
122
|
+
)
|
123
|
+
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
# Save an instantiated record, raising an error on failure.
|
128
|
+
#
|
129
|
+
# returns the record on success; raises an error on failure.
|
130
|
+
def save!
|
131
|
+
save || raise(ArgumentError, 'failed to save!')
|
132
|
+
end
|
133
|
+
|
134
|
+
# Update the data for a record and save.
|
135
|
+
#
|
136
|
+
# data - Hash of data for the record. Required.
|
137
|
+
#
|
138
|
+
# returns #save.
|
139
|
+
def update(data)
|
140
|
+
return false if _id.nil?
|
141
|
+
|
142
|
+
# Copy the existing id to the new data, if it exists.
|
143
|
+
data.merge('_id' => _id)
|
144
|
+
|
145
|
+
# Update the dataset
|
146
|
+
@data = data
|
147
|
+
|
148
|
+
save
|
149
|
+
end
|
150
|
+
|
151
|
+
def _id=(id)
|
152
|
+
@data['_id'] = id
|
153
|
+
end
|
154
|
+
|
155
|
+
def _id
|
156
|
+
@data['_id']
|
157
|
+
end
|
158
|
+
|
159
|
+
# TODO: implement a missing method method for getter/setters
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def _filename
|
164
|
+
'%s.json' % [_id]
|
165
|
+
end
|
166
|
+
|
167
|
+
def _set_id
|
168
|
+
return _id if !_id.nil?
|
169
|
+
|
170
|
+
if self.class._id_generator.nil? || self.class._id_field.nil?
|
171
|
+
self._id = UUIDTools::UUID.random_create.to_s
|
172
|
+
else
|
173
|
+
self._id = self.class._id_generator.call(@data[self.class._id_field])
|
174
|
+
end
|
175
|
+
|
176
|
+
_id
|
177
|
+
end
|
178
|
+
|
179
|
+
def _valid?
|
180
|
+
return false unless @data.keys.map(&:to_s).sort == self.class._schema.keys.map(&:to_s).sort
|
181
|
+
|
182
|
+
@data.each_pair do |key, value|
|
183
|
+
return false unless value.class.to_s == self.class._schema[key.to_s]
|
184
|
+
end
|
185
|
+
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
# When included, extend the class methods of the host class
|
190
|
+
def self.included(host_class)
|
191
|
+
host_class.extend(ClassMethods)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
data/lib/s3db/utils.rb
ADDED
data/s3db.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
files = ['./s3db.gemspec']
|
4
|
+
files << './lib/s3db.rb'
|
5
|
+
|
6
|
+
# Gemfile
|
7
|
+
files << 'Gemfile'
|
8
|
+
files << 'Gemfile.lock'
|
9
|
+
|
10
|
+
|
11
|
+
Gem::Specification.new do |gem|
|
12
|
+
gem.name = "s3db"
|
13
|
+
gem.version = '0.0.0'
|
14
|
+
gem.authors = ["Jack Brown"]
|
15
|
+
gem.email = ["jack@brownjohnf.com"]
|
16
|
+
gem.description = "S3-backed storage engine"
|
17
|
+
gem.summary = "Leverage the endless cheap capacity of s3 for high-latency storage"
|
18
|
+
gem.homepage = "https://github.com/brownjohnf/s3db"
|
19
|
+
|
20
|
+
gem.files = files
|
21
|
+
# Library files
|
22
|
+
gem.files << Dir.glob(File.join('lib', '**', '*.rb'))
|
23
|
+
# Spec files
|
24
|
+
gem.files << Dir.glob(File.join('spec', '**', '*.rb'))
|
25
|
+
# Examples
|
26
|
+
gem.files << Dir.glob(File.join('examples', '**', '*.rb'))
|
27
|
+
gem.executables << 's3db'
|
28
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
29
|
+
gem.require_paths = ["lib"]
|
30
|
+
|
31
|
+
gem.add_dependency 'uuidtools', '~> 2.1', '>= 2.1.5'
|
32
|
+
gem.add_dependency 'json', '~> 1.8', '>= 1.8.3'
|
33
|
+
|
34
|
+
gem.add_development_dependency 'guard-rspec', '~> 4.6', '>= 4.6.4'
|
35
|
+
gem.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0'
|
36
|
+
gem.add_development_dependency 'simplecov', '~> 0.10', '>= 0.10.0'
|
37
|
+
|
38
|
+
gem.license = "MIT"
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe S3DB::Collection do
|
4
|
+
before :each do
|
5
|
+
S3DB::FileBackend.delete(TEST_DB_BASE_PATH)
|
6
|
+
@backend = S3DB::FileBackend.create(TEST_DB_BASE_PATH)
|
7
|
+
@database = S3DB::Database.create(@backend, 'testdb')
|
8
|
+
end
|
9
|
+
|
10
|
+
after :each do
|
11
|
+
S3DB::FileBackend.delete(TEST_DB_BASE_PATH)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '::create' do
|
15
|
+
it 'calls #initialize and #save' do
|
16
|
+
expect(S3DB::Collection).to \
|
17
|
+
receive(:new).with(@database, 'testcoll').and_call_original
|
18
|
+
expect_any_instance_of(S3DB::Collection).to receive(:save)
|
19
|
+
expect(S3DB::Collection.create(@database, 'testcoll')).to \
|
20
|
+
be_a(S3DB::Collection)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'instance methods' do
|
25
|
+
let(:coll_name) { 'testcoll' }
|
26
|
+
let(:schema) do
|
27
|
+
{
|
28
|
+
'id' => 'String'
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
subject do
|
33
|
+
S3DB::Collection.new(@database, coll_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'responds to accessors/readers' do
|
37
|
+
expect(subject).to respond_to(:name)
|
38
|
+
expect(subject).to respond_to(:database)
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#initialize' do
|
42
|
+
it 'sets @database' do
|
43
|
+
expect(subject.instance_variable_get(:@database)).to eq(@database)
|
44
|
+
expect(subject.database).to eq(@database)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'sets @name' do
|
48
|
+
expect(subject.instance_variable_get(:@name)).to eq(coll_name)
|
49
|
+
expect(subject.name).to eq(coll_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'accepts a block and yields itself' do
|
53
|
+
subject
|
54
|
+
|
55
|
+
S3DB::Collection.new(@database, coll_name) do |collection|
|
56
|
+
expect(collection.class).to eq S3DB::Collection
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'accepts an optional block' do
|
61
|
+
expect(S3DB::Collection.new(@database, coll_name)).to \
|
62
|
+
be_a(S3DB::Collection)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'validates itself' do
|
66
|
+
expect_any_instance_of(S3DB::Collection).to receive(:validate!)
|
67
|
+
|
68
|
+
S3DB::Collection.new(@database, coll_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#validate!' do
|
73
|
+
it 'checks the db and raises an error' do
|
74
|
+
expect do
|
75
|
+
S3DB::Collection.new('database', coll_name)
|
76
|
+
end.to raise_error ArgumentError, 'database must be an S3DB::Database!'
|
77
|
+
|
78
|
+
expect do
|
79
|
+
S3DB::Collection.new(@database, coll_name)
|
80
|
+
end.to_not raise_error
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'checks the name and raises an error' do
|
84
|
+
subject.instance_variable_set(:@name, 1)
|
85
|
+
expect do
|
86
|
+
subject.validate!
|
87
|
+
end.to raise_error ArgumentError, 'name must be a String!'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe S3DB::Database do
|
4
|
+
before :each do
|
5
|
+
S3DB::FileBackend.delete(TEST_DB_BASE_PATH)
|
6
|
+
@backend = S3DB::FileBackend.create(TEST_DB_BASE_PATH)
|
7
|
+
end
|
8
|
+
|
9
|
+
after :each do
|
10
|
+
S3DB::FileBackend.delete(TEST_DB_BASE_PATH)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '::create' do
|
14
|
+
it 'writes a new db' do
|
15
|
+
expect(@backend).to receive(:write_db).and_call_original
|
16
|
+
|
17
|
+
S3DB::Database.create(@backend, 'test')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns a db' do
|
21
|
+
expect(S3DB::Database.create(@backend, 'test')).to be_a(S3DB::Database)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '::drop' do
|
26
|
+
before :each do
|
27
|
+
S3DB::Database.create(@backend, 'test')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'calls delete_db on the backend' do
|
31
|
+
expect(@backend).to receive(:delete_db).and_call_original
|
32
|
+
|
33
|
+
S3DB::Database.drop(@backend, 'test')
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not raise an error on a non-existent db' do
|
37
|
+
S3DB::Database.drop(@backend, 'test')
|
38
|
+
|
39
|
+
expect do
|
40
|
+
S3DB::Database.drop(@backend, 'test')
|
41
|
+
end.to_not raise_error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'instance methods' do
|
46
|
+
let(:dbname) { 'test' }
|
47
|
+
|
48
|
+
subject do
|
49
|
+
S3DB::Database.create(@backend, dbname)
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#initialize' do
|
53
|
+
it 'sets the name' do
|
54
|
+
expect(subject.instance_variable_get(:@name)).to eq(dbname)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'sets the database' do
|
58
|
+
expect(subject.instance_variable_get(:@database)).to eq(@database)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'accepts a block and yields itself' do
|
62
|
+
subject
|
63
|
+
|
64
|
+
S3DB::Database.new(subject.backend, subject.name) do |db|
|
65
|
+
expect(db.class).to eq S3DB::Database
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'accepts an optional block' do
|
70
|
+
expect(S3DB::Database.new(subject.backend, subject.name)).to \
|
71
|
+
be_a(S3DB::Database)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#save' do
|
76
|
+
it 'writes the db to disk' do
|
77
|
+
expect(@backend).to receive(:write_db).with(subject.name)
|
78
|
+
expect(subject.save).to eq(subject)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#show_collections' do
|
83
|
+
it 'calls backend.list_collections with the db name' do
|
84
|
+
expect(@backend).to \
|
85
|
+
receive(:list_collections).with(dbname).and_return([])
|
86
|
+
|
87
|
+
subject.show_collections
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'returns an array of strings' do
|
91
|
+
subject.create_collection('testcollection')
|
92
|
+
|
93
|
+
expect(subject.show_collections).to eq %w{testcollection}
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'sorts the collections' do
|
97
|
+
array = %w{a b c}
|
98
|
+
allow(@backend).to receive(:list_collections).and_return(array)
|
99
|
+
|
100
|
+
expect(subject.show_collections).to eq array
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#create_collection' do
|
105
|
+
it 'calls collection::create' do
|
106
|
+
expect(S3DB::Collection).to \
|
107
|
+
receive(:create).with(subject, 'testcollection')
|
108
|
+
|
109
|
+
subject.create_collection('testcollection')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#drop_collection' do
|
114
|
+
it 'calls backend.delete_collection' do
|
115
|
+
expect(@backend).to \
|
116
|
+
receive(:delete_collection).with(dbname, 'testcollection').and_call_original
|
117
|
+
|
118
|
+
subject.drop_collection('testcollection')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#path' do
|
123
|
+
it 'calls backend.db_path' do
|
124
|
+
subject # needed to setup initial calls to db_path
|
125
|
+
|
126
|
+
expect(@backend).to \
|
127
|
+
receive(:db_path).with('test').and_call_original
|
128
|
+
|
129
|
+
expect(subject.path).to eq TEST_DB_BASE_PATH + '/' + dbname
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe 'private #valid?' do
|
134
|
+
it 'returns true if backend.valid_db? is true' do
|
135
|
+
allow(@backend).to receive(:db_exist?).and_return(true)
|
136
|
+
|
137
|
+
expect(subject.__send__(:valid?)).to be true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|