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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a8e1d0962975ce0ce8d619f4e757b60cdf631e82
|
4
|
+
data.tar.gz: 36477336e2e1a6c356cd4ba95430eb61e3d91947
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f691c4bf156243994fa5ca0d0e3532b88f9dfc26442a51e56036d0cec8bd11558a1789bc00a573d0f5e2c93030505da18784c82663d4f7b0c7fdd6bb04abf294
|
7
|
+
data.tar.gz: 33fca3e587a68959a994bd584883426c5169340c2054b325b5a610f307408e6378c3601d8b659aa5f483b1b03e61e3fbd9ec31c05b28b19e2f7c189ab6771e48
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
root = Pathname.new(File.expand_path(__dir__))
|
3
|
+
|
4
|
+
task :irb do
|
5
|
+
ARGV.clear
|
6
|
+
require 'irb'
|
7
|
+
require 'schron'
|
8
|
+
IRB.start
|
9
|
+
end
|
10
|
+
|
11
|
+
task :bundle do
|
12
|
+
system "cd #{root} && bundle install"
|
13
|
+
end
|
14
|
+
|
15
|
+
task :spec do
|
16
|
+
system "cd #{root} && bundle exec rspec"
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => [:bundle, :spec]
|
data/lib/schron.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'schron/archive/interface'
|
2
|
+
require 'schron/dsl'
|
3
|
+
require 'schron/query'
|
4
|
+
|
5
|
+
module Schron
|
6
|
+
module Archive
|
7
|
+
|
8
|
+
def self.included(archive_class)
|
9
|
+
archive_class.extend Schron::DSL
|
10
|
+
end
|
11
|
+
|
12
|
+
include Schron::Archive::Interface
|
13
|
+
|
14
|
+
attr_reader :datastore, :kind, :entity_class, :indexed_fields
|
15
|
+
|
16
|
+
def initialize(datastore,
|
17
|
+
kind: nil,
|
18
|
+
entity_class: nil,
|
19
|
+
indexed_fields: nil)
|
20
|
+
@datastore = datastore
|
21
|
+
@kind = kind || self.class.kind
|
22
|
+
@entity_class = entity_class || self.class.entity_class
|
23
|
+
@indexed_fields = indexed_fields || self.class.indexed_fields
|
24
|
+
yield self if block_given?
|
25
|
+
end
|
26
|
+
|
27
|
+
def query(&block)
|
28
|
+
load_all(Query.new(@kind, @datastore, &block).all)
|
29
|
+
end
|
30
|
+
|
31
|
+
def first(&block)
|
32
|
+
query do |q|
|
33
|
+
yield q if block_given?
|
34
|
+
q.limit(1)
|
35
|
+
end.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def get(id)
|
39
|
+
maybe_load(@datastore.get(@kind, id))
|
40
|
+
end
|
41
|
+
|
42
|
+
def multi_get(ids)
|
43
|
+
load_all(@datastore.multi_get(@kind, ids)).compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def insert(object)
|
47
|
+
maybe_load(@datastore.insert(@kind, dump(object)))
|
48
|
+
end
|
49
|
+
|
50
|
+
def multi_insert(objects)
|
51
|
+
load_all(@datastore.multi_insert(@kind, dump_all(objects)))
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump(object)
|
55
|
+
object.attributes.each_with_object({}) do |(k,v), h|
|
56
|
+
h[dump_value(k)] = dump_value(v)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def load(attributes)
|
61
|
+
attributes.nil? ?
|
62
|
+
nil :
|
63
|
+
entity_class.new(attributes)
|
64
|
+
end
|
65
|
+
|
66
|
+
def dump_all(objects)
|
67
|
+
objects.map { |o| dump(o) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_all(attributes)
|
71
|
+
attributes.map { |attrs| maybe_load(attrs) }
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def maybe_load(attrs)
|
78
|
+
attrs.nil? ? nil : load(attrs)
|
79
|
+
end
|
80
|
+
|
81
|
+
def dump_value(val)
|
82
|
+
val.respond_to?(:attributes) && !val.kind_of?(Class) ?
|
83
|
+
val.attributes :
|
84
|
+
val
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Schron
|
2
|
+
module Archive
|
3
|
+
module Interface
|
4
|
+
|
5
|
+
def query(&block)
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
def first(&block)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(id)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def multi_get(ids)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def insert(object)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def multi_insert(objects)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(attributes)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def dump(object)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def dump_all(objects)
|
38
|
+
objects.map { |o| dump(o) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_all(attributes)
|
42
|
+
attributes.map { |a| load(a) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def identity(object)
|
46
|
+
object.__send__ :id
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'schron/error'
|
2
|
+
module Schron
|
3
|
+
module Datastore
|
4
|
+
module Interface
|
5
|
+
|
6
|
+
def exec_query(kind, query)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(kind, id)
|
11
|
+
multi_get(kind, [id]).first
|
12
|
+
end
|
13
|
+
|
14
|
+
def multi_get(kind, ids)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def insert(kind, hash)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def multi_insert(kind, hashes)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(kind, hash)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def multi_update(kind, hashes)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(kind, id)
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
def multi_remove(kind, ids)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset!
|
43
|
+
protect_reset do
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def protect_reset
|
51
|
+
unless ENV['SCHRON_RESET']
|
52
|
+
raise Schron::OperationDisabledError,
|
53
|
+
"Can not reset datastore with setting ENV['SCHRON_RESET'] to 1"
|
54
|
+
end
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'schron/datastore/interface'
|
3
|
+
|
4
|
+
module Schron
|
5
|
+
module Datastore
|
6
|
+
class Memory
|
7
|
+
include Schron::Datastore::Interface
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@data = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def exec_query(kind, query)
|
14
|
+
results = data_for_kind(kind).values
|
15
|
+
results = apply_filters(query, results)
|
16
|
+
results = apply_sorts(query, results)
|
17
|
+
apply_limit_offset(query, results) || []
|
18
|
+
end
|
19
|
+
|
20
|
+
def multi_get(kind, ids)
|
21
|
+
ids.map { |id| get_data(kind, id) }.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
def insert(kind, hash)
|
25
|
+
hash = hash.merge(id: generate_id) unless hash[:id]
|
26
|
+
set_data(kind, hash[:id], hash)
|
27
|
+
hash.dup
|
28
|
+
end
|
29
|
+
|
30
|
+
def multi_insert(kind, hashes)
|
31
|
+
hashes.map { |h| insert(kind, h) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def update(kind, hash)
|
35
|
+
raise "record must have id to update" unless hash.key?(:id)
|
36
|
+
set_data(kind, hash[:id], hash)
|
37
|
+
hash.dup
|
38
|
+
end
|
39
|
+
|
40
|
+
def multi_update(kind, hashes)
|
41
|
+
raise "records must all have ids to multi update" unless hashes.all?{ |h| h[:id] }
|
42
|
+
hashes.map { |h| update(kind, h) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove(kind, id)
|
46
|
+
data_for_kind(kind).delete(id)
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def multi_remove(kind, ids)
|
51
|
+
ids.each { |id| remove(kind, id) }
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset!
|
56
|
+
protect_reset do
|
57
|
+
@data = {}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def data_for_kind(kind)
|
64
|
+
@data[kind] ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_data(kind, id)
|
68
|
+
data = data_for_kind(kind)[id]
|
69
|
+
data ? data.dup : nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_data(kind, id, data)
|
73
|
+
data_for_kind(kind)[id] = data
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_id
|
77
|
+
SecureRandom.uuid.gsub('-', '')
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def apply_filters(query, data)
|
82
|
+
query.filters.reduce(data) do |data, (field, op, filter_value)|
|
83
|
+
case op
|
84
|
+
when '='
|
85
|
+
data.select { |obj| obj[field] == filter_value }
|
86
|
+
when '!='
|
87
|
+
data.select { |obj| obj[field] != filter_value }
|
88
|
+
when 'in'
|
89
|
+
data.select { |obj| filter_value.include?(obj[field]) }
|
90
|
+
when '>'
|
91
|
+
data.select { |obj| obj[field] && obj[field] > filter_value }
|
92
|
+
when '<'
|
93
|
+
data.select { |obj| obj[field] && obj[field] < filter_value }
|
94
|
+
when '>='
|
95
|
+
data.select { |obj| obj[field] && obj[field] >= filter_value }
|
96
|
+
when '<='
|
97
|
+
data.select { |obj| obj[field] && obj[field] <= filter_value }
|
98
|
+
else
|
99
|
+
raise "unsupported op #{op}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def apply_sorts(query, data)
|
105
|
+
data.sort { |a, b| compare_records(a, b, query.sorts) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def compare_records(record1, record2, sorts)
|
109
|
+
sorts.each do |sort|
|
110
|
+
result = compare_record record1, record2, sort
|
111
|
+
return result if result
|
112
|
+
end
|
113
|
+
0
|
114
|
+
end
|
115
|
+
|
116
|
+
def compare_record(record1, record2, sort)
|
117
|
+
field, direction = sort
|
118
|
+
field1, field2 = record1[field], record2[field]
|
119
|
+
if field1 == field2
|
120
|
+
nil
|
121
|
+
elsif field1.nil?
|
122
|
+
direction == :asc ? -1 : 1
|
123
|
+
elsif field2.nil?
|
124
|
+
direction == :asc ? 1 : -1
|
125
|
+
elsif field1 < field2 && direction == :asc
|
126
|
+
-1
|
127
|
+
elsif field1 > field2 && direction == :desc
|
128
|
+
-1
|
129
|
+
else
|
130
|
+
1
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def apply_limit_offset(query, data)
|
136
|
+
if query.limit && query.offset
|
137
|
+
data[query.offset...(query.offset+query.limit)]
|
138
|
+
elsif query.limit
|
139
|
+
data[0...query.limit]
|
140
|
+
elsif query.offset
|
141
|
+
data[query.offset..data.size]
|
142
|
+
else
|
143
|
+
data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
require 'schron/id'
|
3
|
+
require 'schron/util'
|
4
|
+
require 'schron/datastore/interface'
|
5
|
+
require 'schron/datastore/serializer'
|
6
|
+
|
7
|
+
module Schron
|
8
|
+
module Datastore
|
9
|
+
class Mongo
|
10
|
+
include Schron::Datastore::Interface
|
11
|
+
|
12
|
+
def initialize(db)
|
13
|
+
@db = db
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(kind, id)
|
17
|
+
doc = coll(kind).find(id_selector(id)).first
|
18
|
+
doc ?
|
19
|
+
unserialize(kind, doc) :
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def multi_get(kind, ids)
|
24
|
+
docs = coll(kind).find(multiple_id_selector(ids))
|
25
|
+
Schron::Util.sorted_by_id_list(docs.map{ |doc| unserialize(kind, doc) }, ids)
|
26
|
+
end
|
27
|
+
|
28
|
+
def insert(kind, hash)
|
29
|
+
hash[:id] ||= Schron::Id.generate
|
30
|
+
serialized = serialize(kind, hash)
|
31
|
+
coll(kind).insert(serialized)
|
32
|
+
hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def multi_insert(kind, hashes)
|
36
|
+
hashes.each { |h| h[:id] ||= Schron::Id.generate }
|
37
|
+
docs = hashes.map { |h| serialize(kind, h) }
|
38
|
+
coll(kind).insert(docs)
|
39
|
+
hashes
|
40
|
+
end
|
41
|
+
|
42
|
+
def update(kind, hash)
|
43
|
+
Schron::Id.require!(hash)
|
44
|
+
doc = serialize(kind, hash)
|
45
|
+
coll(kind).update(id_selector(hash[:id]), doc)
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def multi_update(kind, hashes)
|
50
|
+
Schron::Id.require_all!(hashes)
|
51
|
+
hashes.each do |hash|
|
52
|
+
doc = serialize(kind, hash)
|
53
|
+
coll(kind).update(id_selector(hash[:id]), doc)
|
54
|
+
end
|
55
|
+
hashes
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove(kind, id)
|
59
|
+
coll(kind).remove(id_selector(id))
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def multi_remove(kind, ids)
|
64
|
+
coll(kind).remove(multiple_id_selector(ids))
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def exec_query(kind, query)
|
69
|
+
selector = selector_for(query)
|
70
|
+
query_opts = query_opts_for(query)
|
71
|
+
docs = coll(kind).find(selector, query_opts)
|
72
|
+
docs.map{ |d| unserialize(kind, d) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset!
|
76
|
+
protect_reset do
|
77
|
+
@db.collections.each do |coll|
|
78
|
+
unless coll.name =~ /^system\./
|
79
|
+
coll.drop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def coll(kind)
|
88
|
+
@db[kind]
|
89
|
+
end
|
90
|
+
|
91
|
+
def serializer
|
92
|
+
@serializer ||= Schron::Datastore::Serializer.new
|
93
|
+
end
|
94
|
+
|
95
|
+
def serialize(kind, hash)
|
96
|
+
doc = hash.merge(_id: serialize_id(hash[:id]))
|
97
|
+
doc.delete(:id)
|
98
|
+
serializer.serialize(kind, doc)
|
99
|
+
end
|
100
|
+
|
101
|
+
def unserialize(kind, doc)
|
102
|
+
hash = Schron::Util.symbolize_keys(doc)
|
103
|
+
hash.merge! id: hash[:_id].to_s
|
104
|
+
hash.delete(:_id)
|
105
|
+
serializer.unserialize(kind, hash)
|
106
|
+
end
|
107
|
+
|
108
|
+
def serialize_id(uuid)
|
109
|
+
uuid = uuid.dup if uuid.frozen?
|
110
|
+
BSON::Binary.new(uuid, BSON::Binary::SUBTYPE_UUID)
|
111
|
+
end
|
112
|
+
|
113
|
+
def unserialize_id(bin)
|
114
|
+
bin.to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
def id_selector(id)
|
118
|
+
{_id: serialize_id(id)}
|
119
|
+
end
|
120
|
+
|
121
|
+
def multiple_id_selector(ids)
|
122
|
+
serialized = ids.map { |id| serialize_id(id) }
|
123
|
+
{_id: {"$in" => serialized }}
|
124
|
+
end
|
125
|
+
|
126
|
+
def selector_for(query)
|
127
|
+
selector = {}
|
128
|
+
query.filters.each do |(field, op, filter_value)|
|
129
|
+
mongo_field = mongoize_field(field)
|
130
|
+
case op
|
131
|
+
when '='
|
132
|
+
selector[mongo_field] = filter_value
|
133
|
+
when '!='
|
134
|
+
selector[mongo_field] = { "$ne" => filter_value }
|
135
|
+
when 'in'
|
136
|
+
selector[mongo_field] = { "$in" => filter_value.to_a }
|
137
|
+
when '>'
|
138
|
+
selector[mongo_field] = { "$gt" => filter_value }
|
139
|
+
when '<'
|
140
|
+
selector[mongo_field] = { "$lt" => filter_value }
|
141
|
+
when '>='
|
142
|
+
selector[mongo_field] = { "$gte" => filter_value }
|
143
|
+
when '<='
|
144
|
+
selector[mongo_field] = { "$lte" => filter_value }
|
145
|
+
else
|
146
|
+
raise "unsupported op #{op}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
selector
|
150
|
+
end
|
151
|
+
|
152
|
+
def query_opts_for(query)
|
153
|
+
{}.tap do |opts|
|
154
|
+
opts[:sort] = query_sorts(query)
|
155
|
+
opts[:limit] = query.limit
|
156
|
+
opts[:skip] = query.offset
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def query_sorts(query)
|
161
|
+
query.sorts.map do |(field, direction)|
|
162
|
+
mongo_dir = (direction == :asc ? ::Mongo::ASCENDING : ::Mongo::DESCENDING)
|
163
|
+
[mongoize_field(field), mongo_dir]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def mongoize_field(field_name)
|
168
|
+
field_name == :id ? :_id : field_name
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|