schron 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +4 -0
  4. data/Rakefile +19 -0
  5. data/lib/schron.rb +4 -0
  6. data/lib/schron/archive.rb +88 -0
  7. data/lib/schron/archive/interface.rb +52 -0
  8. data/lib/schron/datastore/interface.rb +60 -0
  9. data/lib/schron/datastore/memory.rb +152 -0
  10. data/lib/schron/datastore/mongo.rb +173 -0
  11. data/lib/schron/datastore/mongo/metadata.rb +49 -0
  12. data/lib/schron/datastore/mongo/serializer.rb +38 -0
  13. data/lib/schron/datastore/sequel.rb +137 -0
  14. data/lib/schron/datastore/serializer.rb +46 -0
  15. data/lib/schron/dsl.rb +28 -0
  16. data/lib/schron/error.rb +7 -0
  17. data/lib/schron/id.rb +22 -0
  18. data/lib/schron/identity_map.rb +26 -0
  19. data/lib/schron/paginated_results.rb +28 -0
  20. data/lib/schron/paging.rb +5 -0
  21. data/lib/schron/query.rb +122 -0
  22. data/lib/schron/repository.rb +35 -0
  23. data/lib/schron/repository/interface.rb +36 -0
  24. data/lib/schron/test.rb +22 -0
  25. data/lib/schron/test/archive_examples.rb +133 -0
  26. data/lib/schron/test/datastore_examples.rb +419 -0
  27. data/lib/schron/test/entity.rb +21 -0
  28. data/lib/schron/test/repository_examples.rb +68 -0
  29. data/lib/schron/util.rb +27 -0
  30. data/schron.gemspec +24 -0
  31. data/spec/lib/schron/archive_spec.rb +26 -0
  32. data/spec/lib/schron/datastore/memory_spec.rb +9 -0
  33. data/spec/lib/schron/datastore/mongo_spec.rb +23 -0
  34. data/spec/lib/schron/datastore/sequel_spec.rb +40 -0
  35. data/spec/lib/schron/dsl_spec.rb +46 -0
  36. data/spec/lib/schron/identity_map_spec.rb +36 -0
  37. data/spec/lib/schron/paginaged_results_spec.rb +27 -0
  38. data/spec/lib/schron/query_spec.rb +46 -0
  39. data/spec/lib/schron/repository_spec.rb +27 -0
  40. data/spec/spec_helper.rb +9 -0
  41. data/spec/support/test_entities.rb +15 -0
  42. 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
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ bin
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+
4
+ gemspec
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,4 @@
1
+ # Dir[File.join(__dir__, '**/*.rb')].each { |f| require f }
2
+
3
+ require 'schron/repository'
4
+ require 'schron/archive'
@@ -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