schron 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/Rakefile +19 -0
- data/lib/schron.rb +4 -0
- data/lib/schron/archive.rb +88 -0
- data/lib/schron/archive/interface.rb +52 -0
- data/lib/schron/datastore/interface.rb +60 -0
- data/lib/schron/datastore/memory.rb +152 -0
- data/lib/schron/datastore/mongo.rb +173 -0
- data/lib/schron/datastore/mongo/metadata.rb +49 -0
- data/lib/schron/datastore/mongo/serializer.rb +38 -0
- data/lib/schron/datastore/sequel.rb +137 -0
- data/lib/schron/datastore/serializer.rb +46 -0
- data/lib/schron/dsl.rb +28 -0
- data/lib/schron/error.rb +7 -0
- data/lib/schron/id.rb +22 -0
- data/lib/schron/identity_map.rb +26 -0
- data/lib/schron/paginated_results.rb +28 -0
- data/lib/schron/paging.rb +5 -0
- data/lib/schron/query.rb +122 -0
- data/lib/schron/repository.rb +35 -0
- data/lib/schron/repository/interface.rb +36 -0
- data/lib/schron/test.rb +22 -0
- data/lib/schron/test/archive_examples.rb +133 -0
- data/lib/schron/test/datastore_examples.rb +419 -0
- data/lib/schron/test/entity.rb +21 -0
- data/lib/schron/test/repository_examples.rb +68 -0
- data/lib/schron/util.rb +27 -0
- data/schron.gemspec +24 -0
- data/spec/lib/schron/archive_spec.rb +26 -0
- data/spec/lib/schron/datastore/memory_spec.rb +9 -0
- data/spec/lib/schron/datastore/mongo_spec.rb +23 -0
- data/spec/lib/schron/datastore/sequel_spec.rb +40 -0
- data/spec/lib/schron/dsl_spec.rb +46 -0
- data/spec/lib/schron/identity_map_spec.rb +36 -0
- data/spec/lib/schron/paginaged_results_spec.rb +27 -0
- data/spec/lib/schron/query_spec.rb +46 -0
- data/spec/lib/schron/repository_spec.rb +27 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/test_entities.rb +15 -0
- metadata +192 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Schron
|
2
|
+
module Test
|
3
|
+
class Entity
|
4
|
+
|
5
|
+
attr_reader :attributes
|
6
|
+
def initialize(attrs={})
|
7
|
+
@attributes = attrs
|
8
|
+
end
|
9
|
+
|
10
|
+
def id
|
11
|
+
attributes[:id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(entity)
|
15
|
+
entity.is_a?(self.class) && attributes == entity.attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
if defined?(RSpec)
|
2
|
+
|
3
|
+
require 'schron/query'
|
4
|
+
|
5
|
+
# Be sure to set the repo before inclusion:
|
6
|
+
# let(:repo) { ... }
|
7
|
+
shared_examples 'schron repository' do
|
8
|
+
|
9
|
+
let(:entity_class) { repo.entity_class }
|
10
|
+
|
11
|
+
describe 'update' do
|
12
|
+
it 'returns the updated object' do
|
13
|
+
object = repo.insert(repo.entity_class.new)
|
14
|
+
object.attributes[:a] = 6
|
15
|
+
|
16
|
+
result = repo.update(object)
|
17
|
+
|
18
|
+
expect_same_entity result, repo.entity_class.new(id: object.id, a: 6)
|
19
|
+
expect_same_entity repo.get(result.id), result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'multi update' do
|
24
|
+
it 'returns the updated objects' do
|
25
|
+
objects = repo.multi_insert([repo.entity_class.new, repo.entity_class.new])
|
26
|
+
objects[0].attributes[:a] = 6
|
27
|
+
objects[1].attributes[:a] = 7
|
28
|
+
|
29
|
+
results = repo.multi_update(objects)
|
30
|
+
|
31
|
+
expect(results[0].attributes).to eq(id: objects[0].id, a: 6)
|
32
|
+
expect(results[1].attributes).to eq(id: objects[1].id, a: 7)
|
33
|
+
0.upto(1) do |i|
|
34
|
+
expect_same_entity repo.get(results[i].id), results[i]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'remove' do
|
40
|
+
it 'returns nil when no object is removed' do
|
41
|
+
expect(repo.remove('1234')).to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns nil when an object is removed' do
|
45
|
+
obj = repo.insert(repo.entity_class.new)
|
46
|
+
expect(repo.remove(obj.id)).to be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'removes the object from the repository by id' do
|
50
|
+
obj = repo.insert(repo.entity_class.new)
|
51
|
+
repo.remove(obj.id)
|
52
|
+
expect(repo.get(obj.id)).to be_nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'multi remove' do
|
57
|
+
it 'removes the objects from the repository by id and returns nil' do
|
58
|
+
objects = repo.multi_insert([entity_class.new, entity_class.new])
|
59
|
+
ids = objects.map(&:id)
|
60
|
+
result = repo.multi_remove(ids)
|
61
|
+
expect(result).to be_nil
|
62
|
+
expect(repo.multi_get(ids)).to be_empty
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/lib/schron/util.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Schron
|
2
|
+
module Util
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def symbolize_keys(obj)
|
6
|
+
case obj
|
7
|
+
when Hash
|
8
|
+
obj.reduce({}) do |symbolized, (k, v)|
|
9
|
+
symbolized[k.to_sym] = symbolize_keys(v)
|
10
|
+
symbolized
|
11
|
+
end
|
12
|
+
when Array
|
13
|
+
obj.map { |v| symbolize_keys(v) }
|
14
|
+
else
|
15
|
+
obj
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def sorted_by_id_list(records, ids)
|
20
|
+
if ids.respond_to?(:index)
|
21
|
+
records.sort_by { |r| ids.index(r[:id]) }
|
22
|
+
else
|
23
|
+
records
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/schron.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "schron"
|
7
|
+
spec.version = "0.0.2"
|
8
|
+
spec.authors = ["David Faber"]
|
9
|
+
spec.email = ["david@1bios.co"]
|
10
|
+
spec.summary = %q{Repository implementation for entity persistence}
|
11
|
+
|
12
|
+
spec.files = `git ls-files -z`.split("\x0")
|
13
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
|
16
|
+
|
17
|
+
spec.add_development_dependency 'rake', '~> 10.1.0'
|
18
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
19
|
+
spec.add_development_dependency "rspec", "~> 3.0.0.beta1"
|
20
|
+
spec.add_development_dependency 'sequel', '~> 4.9.0'
|
21
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3.9'
|
22
|
+
spec.add_development_dependency 'mongo', '~> 1.10.0'
|
23
|
+
spec.add_development_dependency 'bson_ext', '~> 1.10.0'
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/archive'
|
3
|
+
require 'schron/datastore/memory'
|
4
|
+
|
5
|
+
describe Schron::Archive do
|
6
|
+
|
7
|
+
subject(:archive) do
|
8
|
+
archive_class.new(ds,
|
9
|
+
kind: 'test',
|
10
|
+
entity_class: Schron::Test::Entity,
|
11
|
+
indexed_fields: [])
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:archive_class) do
|
15
|
+
Class.new do
|
16
|
+
include Schron::Archive
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:ds) do
|
21
|
+
Schron::Datastore::Memory.new
|
22
|
+
end
|
23
|
+
|
24
|
+
include_examples 'schron archive'
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/datastore/mongo'
|
3
|
+
|
4
|
+
describe Schron::Datastore::Mongo do
|
5
|
+
let(:ds) { described_class.new(db) }
|
6
|
+
|
7
|
+
let(:db) { client[db_name] }
|
8
|
+
|
9
|
+
let(:db_name) { 'test' }
|
10
|
+
|
11
|
+
let(:client) { Mongo::MongoClient.new }
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
db.collections.each do |coll|
|
15
|
+
unless coll.name =~ /^system\./
|
16
|
+
coll.drop
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
include_examples 'schron datastore'
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
require 'schron/datastore/sequel'
|
4
|
+
|
5
|
+
describe Schron::Datastore::Sequel do
|
6
|
+
subject(:ds) { described_class.new(db) }
|
7
|
+
|
8
|
+
let(:db) { Sequel.sqlite(':memory:', logger: LOGGER) }
|
9
|
+
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
db.create_table :entities do
|
13
|
+
String :id, primary_key: true
|
14
|
+
end
|
15
|
+
db.create_table :named do
|
16
|
+
String :id, primary_key: true
|
17
|
+
String :name
|
18
|
+
end
|
19
|
+
db.create_table :with_types do
|
20
|
+
String :id, primary_key: true
|
21
|
+
String :array
|
22
|
+
String :set
|
23
|
+
Date :date
|
24
|
+
Time :time
|
25
|
+
String :date_time
|
26
|
+
String :hash
|
27
|
+
String :children
|
28
|
+
Float :float
|
29
|
+
String :name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
after(:each) do
|
34
|
+
db.drop_table? :entities
|
35
|
+
db.drop_table? :named
|
36
|
+
end
|
37
|
+
|
38
|
+
include_examples 'schron datastore'
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/dsl'
|
3
|
+
|
4
|
+
describe Schron::DSL do
|
5
|
+
let(:repo_class) do
|
6
|
+
Class.new do
|
7
|
+
extend Schron::DSL
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
describe 'kind' do
|
13
|
+
it 'should set and get on the class' do
|
14
|
+
repo_class.kind 'testing'
|
15
|
+
expect(repo_class.kind).to eq('testing')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should not be inherited by subclasses' do
|
19
|
+
repo_class.kind 'testing'
|
20
|
+
sub = Class.new(repo_class)
|
21
|
+
expect(sub.kind).to be_nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'entity class' do
|
26
|
+
it 'should set and get on the class' do
|
27
|
+
klass = Class.new
|
28
|
+
repo_class.entity_class klass
|
29
|
+
expect(repo_class.entity_class).to be(klass)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should not be inherited by subclasses' do
|
33
|
+
repo_class.entity_class(Object)
|
34
|
+
sub = Class.new(repo_class)
|
35
|
+
expect(sub.entity_class).to be_nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'indexed fields' do
|
40
|
+
it 'sets one at a time with indexed_field, and gets with indexed_fields' do
|
41
|
+
repo_class.indexed_field :a
|
42
|
+
repo_class.indexed_field :b
|
43
|
+
expect(repo_class.indexed_fields).to eq([:a, :b])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/identity_map'
|
3
|
+
|
4
|
+
describe Schron::IdentityMap do
|
5
|
+
subject(:map) { described_class.new }
|
6
|
+
|
7
|
+
let(:object) { Object.new }
|
8
|
+
|
9
|
+
describe 'put and get' do
|
10
|
+
it 'shoudl return nil if no object exists with the id' do
|
11
|
+
expect(map.get(123)).to be_nil
|
12
|
+
end
|
13
|
+
it 'should put things in and take things out by id' do
|
14
|
+
map.put(1, object)
|
15
|
+
expect(map.get(1)).to be(object)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'fetch' do
|
20
|
+
it 'should return objects already in the map without calling the block' do
|
21
|
+
spy = double
|
22
|
+
expect(spy).not_to receive(:uh_oh)
|
23
|
+
map.put(1, object)
|
24
|
+
fetched = map.fetch(1) { double.uh_oh }
|
25
|
+
expect(fetched).to be(object)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'has?' do
|
30
|
+
it 'returns true if the map has an entry for the given key, else false' do
|
31
|
+
expect(map.has?(1)).to be(false)
|
32
|
+
map.put(1, object)
|
33
|
+
expect(map.has?(1)).to be(true)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/paginated_results'
|
3
|
+
|
4
|
+
describe Schron::PaginatedResults do
|
5
|
+
|
6
|
+
let(:results) do
|
7
|
+
ary = described_class.new([1,2])
|
8
|
+
ary.paging.previous_page = 1
|
9
|
+
ary.paging.current_page = 2
|
10
|
+
ary.paging.next_page = 3
|
11
|
+
ary
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'closure of enumerable operations' do
|
15
|
+
|
16
|
+
def verify_paging(p)
|
17
|
+
expect(p.previous_page).to eq(1)
|
18
|
+
expect(p.current_page).to eq(2)
|
19
|
+
expect(p.next_page).to eq(3)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'persists its paging' do
|
23
|
+
p = results.map {|i| i}.paging
|
24
|
+
verify_paging(p)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/query'
|
3
|
+
require 'schron/datastore/memory'
|
4
|
+
|
5
|
+
describe Schron::Query do
|
6
|
+
subject(:query) { described_class.new(kind, ds) }
|
7
|
+
|
8
|
+
let(:kind) { 'tests' }
|
9
|
+
let(:ds) { Schron::Datastore::Memory.new }
|
10
|
+
|
11
|
+
describe 'page' do
|
12
|
+
it 'sets the limit and offset' do
|
13
|
+
query.page(3, per_page: 7)
|
14
|
+
expect(query.limit).to eq(7)
|
15
|
+
expect(query.offset).to eq(14)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'paginated result set' do
|
19
|
+
before(:each) do
|
20
|
+
10.times { ds.insert('tests', {}) }
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'has the current page' do
|
24
|
+
results = query.page(3, per_page: 1).all
|
25
|
+
expect(results.paging.current_page).to eq(3)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'indicates next and previous page when present' do
|
29
|
+
query.page(2, per_page: 3)
|
30
|
+
results = query.all
|
31
|
+
expect(results.paging.previous_page).to eq(1)
|
32
|
+
expect(results.paging.next_page).to eq(3)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'shows previous page as nil on the first page' do
|
36
|
+
results = query.page(1).all
|
37
|
+
expect(results.paging.previous_page).to be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'shows next page as nil on last page' do
|
41
|
+
results = query.page(1, per_page: 10).all
|
42
|
+
expect(results.paging.next_page).to be_nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schron/repository'
|
3
|
+
require 'schron/datastore/memory'
|
4
|
+
|
5
|
+
describe Schron::Repository do
|
6
|
+
include Schron::Test
|
7
|
+
|
8
|
+
subject(:repo) do
|
9
|
+
repo_class.new(ds,
|
10
|
+
entity_class: Schron::Test::Entity,
|
11
|
+
kind: 'tests')
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:archive) { repo }
|
15
|
+
let(:ds) { Schron::Datastore::Memory.new }
|
16
|
+
|
17
|
+
let(:repo_class) do
|
18
|
+
Class.new do
|
19
|
+
include Schron::Repository
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include_examples 'schron archive'
|
24
|
+
include_examples 'schron repository'
|
25
|
+
|
26
|
+
|
27
|
+
end
|