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.
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