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.
@@ -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
+
@@ -0,0 +1,11 @@
1
+ module S3DB
2
+ class Utils
3
+ class << self
4
+ def sanitize(input)
5
+ raise ArgumentError, 'invalid input!' unless input =~ /^\w+$/i
6
+
7
+ input.to_s
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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