schron 0.0.2
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/.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
|