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,49 @@
|
|
1
|
+
# require 'schron/datastore/metadata'
|
2
|
+
|
3
|
+
module Schron
|
4
|
+
module Datastore
|
5
|
+
class Mongo
|
6
|
+
class Metadata < Schron::Datastore::Metadata
|
7
|
+
|
8
|
+
NATIVELY_SUPPORTED = [
|
9
|
+
String,
|
10
|
+
Integer,
|
11
|
+
Fixnum,
|
12
|
+
Float,
|
13
|
+
Array,
|
14
|
+
Regexp,
|
15
|
+
TrueClass,
|
16
|
+
FalseClass,
|
17
|
+
Time,
|
18
|
+
Hash,
|
19
|
+
Symbol,
|
20
|
+
BSON::Binary
|
21
|
+
]
|
22
|
+
|
23
|
+
def initialize(db, collection_name=:schron_metadata)
|
24
|
+
@collection = db[collection_name]
|
25
|
+
end
|
26
|
+
|
27
|
+
def recursive?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def natively_supported?(value)
|
32
|
+
NATIVELY_SUPPORTED.include?(value.class)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Array] an array of hashes, each with the following keys:
|
36
|
+
# * kind
|
37
|
+
# * field
|
38
|
+
# * type
|
39
|
+
def fetch_metadata
|
40
|
+
@collection.find
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert_metadata(kind, field, type)
|
44
|
+
@collection.insert(kind: kind, field: field, type: type)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Schron
|
2
|
+
module Datastore
|
3
|
+
class Mongo
|
4
|
+
class Serializer
|
5
|
+
|
6
|
+
def serialize_document(hash)
|
7
|
+
doc = hash.merge(_id: serialize_id(hash[:id]))
|
8
|
+
doc.delete :id
|
9
|
+
doc
|
10
|
+
end
|
11
|
+
|
12
|
+
def unserialize_document(doc)
|
13
|
+
hash = symbolize_keys(doc)
|
14
|
+
hash.merge!(id: unserialize_id(hash[:_id]))
|
15
|
+
hash.delete :_id
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def serialize_id(uuid)
|
20
|
+
BSON::Binary.new(uuid, BSON::Binary::SUBTYPE_UUID)
|
21
|
+
end
|
22
|
+
|
23
|
+
def unserialize_id(bin)
|
24
|
+
bin.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def symbolize_keys(hash)
|
28
|
+
hash.reduce({}) do |symbolized, (k, v)|
|
29
|
+
val = v.kind_of?(Hash) ? symbolize_keys(v) : v
|
30
|
+
symbolized[k.to_sym] = val
|
31
|
+
symbolized
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'json'
|
3
|
+
require 'schron/datastore/interface'
|
4
|
+
require 'schron/id'
|
5
|
+
require 'schron/datastore/serializer'
|
6
|
+
|
7
|
+
module Schron
|
8
|
+
module Datastore
|
9
|
+
class Sequel
|
10
|
+
|
11
|
+
include Schron::Datastore::Interface
|
12
|
+
|
13
|
+
MAX_LIMIT = 2**61
|
14
|
+
|
15
|
+
def initialize(db)
|
16
|
+
@db = db
|
17
|
+
end
|
18
|
+
|
19
|
+
def exec_query(kind, query)
|
20
|
+
dataset = @db[kind]
|
21
|
+
dataset = apply_filters(query, dataset)
|
22
|
+
dataset = apply_sorts(query, dataset)
|
23
|
+
dataset = apply_limit_and_offset(query, dataset)
|
24
|
+
dataset.all
|
25
|
+
end
|
26
|
+
|
27
|
+
def multi_get(kind, ids)
|
28
|
+
results = unserialize_all(kind, @db[kind].where(id: ids).all)
|
29
|
+
Schron::Util.sorted_by_id_list(results, ids)
|
30
|
+
end
|
31
|
+
|
32
|
+
def insert(kind, hash)
|
33
|
+
hash[:id] = Schron::Id.generate unless hash[:id]
|
34
|
+
@db[kind].insert(serialize(kind, hash))
|
35
|
+
hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def multi_insert(kind, hashes)
|
39
|
+
hashes.each do |h|
|
40
|
+
h[:id] = Schron::Id.generate unless h[:id]
|
41
|
+
end
|
42
|
+
@db[kind].multi_insert(serialize_all(kind, hashes))
|
43
|
+
hashes
|
44
|
+
end
|
45
|
+
|
46
|
+
def update(kind, hash)
|
47
|
+
raise "must provide an id" unless hash.key?(:id)
|
48
|
+
@db[kind].where(id: hash[:id]).update(serialize(kind, hash))
|
49
|
+
hash
|
50
|
+
end
|
51
|
+
|
52
|
+
def multi_update(kind, hashes)
|
53
|
+
raise "records must all have ids to multi update" unless hashes.all?{ |h| h[:id] }
|
54
|
+
hashes.map { |h| update(kind, h) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def remove(kind, id)
|
58
|
+
@db[kind].where(id: id).delete
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def multi_remove(kind, ids)
|
63
|
+
@db[kind].where(id: ids).delete
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset!
|
68
|
+
protect_reset do
|
69
|
+
@db.tables.each do |table|
|
70
|
+
@db.drop_table(table)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def serializer
|
78
|
+
@serializer ||= Schron::Datastore::Serializer.new
|
79
|
+
end
|
80
|
+
|
81
|
+
def serialize(kind, hash)
|
82
|
+
serializer.serialize(kind, hash)
|
83
|
+
end
|
84
|
+
|
85
|
+
def serialize_all(kind, hashes)
|
86
|
+
hashes.map{ |h| serialize(kind, h) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def unserialize(kind, hash)
|
90
|
+
serializer.unserialize(kind, hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
def unserialize_all(kind, hashes)
|
94
|
+
hashes.map { |h| unserialize(kind, h) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_filters(query, data)
|
98
|
+
final = query.filters.reduce(data) do |data, (name, op, value)|
|
99
|
+
case op
|
100
|
+
when '='
|
101
|
+
data.where name => value
|
102
|
+
when 'in'
|
103
|
+
data = data.where name => value
|
104
|
+
data = data.or(name => nil) if value.include?(nil)
|
105
|
+
data
|
106
|
+
when '!='
|
107
|
+
data = data.exclude(name: value)
|
108
|
+
data = data.or(name: nil) unless value.nil?
|
109
|
+
data
|
110
|
+
when '>', '<', '>=', '<='
|
111
|
+
data.where "#{name} #{op} ?", value
|
112
|
+
else
|
113
|
+
raise "unsupported op #{op}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def apply_limit_and_offset(query, data)
|
119
|
+
if query.limit && query.offset
|
120
|
+
data.limit(query.limit, query.offset)
|
121
|
+
elsif query.limit
|
122
|
+
data.limit(query.limit)
|
123
|
+
elsif query.offset
|
124
|
+
data.limit(MAX_LIMIT, query.offset)
|
125
|
+
else
|
126
|
+
data
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_sorts(query, data)
|
131
|
+
query.sorts.reduce(data) do |data, (field, dir)|
|
132
|
+
data.order_append(dir == :asc ? ::Sequel.asc(field) : ::Sequel.desc(field))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
require 'set'
|
4
|
+
require 'schron/util'
|
5
|
+
module Schron
|
6
|
+
module Datastore
|
7
|
+
class Serializer
|
8
|
+
|
9
|
+
# Make this thing into a Hash of Arrays of Floats, ints and strings
|
10
|
+
|
11
|
+
def serialize(kind, hash)
|
12
|
+
hash.each_with_object({}) do |(field, value), h|
|
13
|
+
h[field] = serialize_value(value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def unserialize(kind, hash)
|
18
|
+
Schron::Util.symbolize_keys(hash)
|
19
|
+
# hash.each_with_object({}) do |(field, value), hash|
|
20
|
+
# hash[field] = unserialize_value(value)
|
21
|
+
# end
|
22
|
+
# )
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def serialize_value(value)
|
28
|
+
case value
|
29
|
+
when Hash
|
30
|
+
value.each_with_object({}) do |(key, val), serialized|
|
31
|
+
serialized[key] = serialize_value(val)
|
32
|
+
end
|
33
|
+
when Array, Set
|
34
|
+
value.map { |v| serialize_value(v) }.to_a
|
35
|
+
when Date, DateTime, Time
|
36
|
+
value.to_datetime.iso8601(9)
|
37
|
+
when BigDecimal
|
38
|
+
value.to_f
|
39
|
+
else
|
40
|
+
value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/schron/dsl.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Schron
|
2
|
+
module DSL
|
3
|
+
|
4
|
+
def kind(kind=nil)
|
5
|
+
set_or_get(:kind, kind)
|
6
|
+
end
|
7
|
+
|
8
|
+
def entity_class(entity_class=nil)
|
9
|
+
set_or_get(:entity_class, entity_class)
|
10
|
+
end
|
11
|
+
|
12
|
+
def indexed_field(field)
|
13
|
+
(@indexed_fields ||= []) << field
|
14
|
+
end
|
15
|
+
|
16
|
+
def indexed_fields
|
17
|
+
@indexed_fields || []
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def set_or_get(name, val)
|
23
|
+
val.nil? ?
|
24
|
+
instance_variable_get("@#{name}") :
|
25
|
+
instance_variable_set("@#{name}", val)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/schron/error.rb
ADDED
data/lib/schron/id.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
module Schron
|
3
|
+
module Id
|
4
|
+
module_function
|
5
|
+
def generate
|
6
|
+
SecureRandom.uuid
|
7
|
+
end
|
8
|
+
|
9
|
+
def require!(obj)
|
10
|
+
id_present = if obj.kind_of?(Hash)
|
11
|
+
obj[:id]
|
12
|
+
else
|
13
|
+
obj.id
|
14
|
+
end
|
15
|
+
raise "must provide an id" unless id_present
|
16
|
+
end
|
17
|
+
|
18
|
+
def require_all!(objects)
|
19
|
+
objects.each { |o| require!(o) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Schron
|
2
|
+
class IdentityMap
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@data = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def put(id, object)
|
9
|
+
@data[id] = object
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(id)
|
13
|
+
@data[id]
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch(id, &block)
|
17
|
+
put(id, block.call) unless has?(id)
|
18
|
+
get(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def has?(id)
|
22
|
+
@data.key?(id)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'schron/paging'
|
2
|
+
|
3
|
+
module Schron
|
4
|
+
class PaginatedResults < Array
|
5
|
+
|
6
|
+
attr_reader :paging
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
@paging = Paging.new
|
11
|
+
end
|
12
|
+
|
13
|
+
[:map].each do |method|
|
14
|
+
define_method(method) do |*args, &block|
|
15
|
+
results = self.class.new(super(*args, &block))
|
16
|
+
results.paging = paging.dup
|
17
|
+
results
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def paging=(paging)
|
24
|
+
@paging = paging
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/schron/query.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'schron/paginated_results'
|
2
|
+
module Schron
|
3
|
+
class InvalidQueryError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class Query
|
7
|
+
attr_reader :filters
|
8
|
+
attr_reader :sorts
|
9
|
+
attr_reader :kind
|
10
|
+
|
11
|
+
def initialize(kind=nil, datastore=nil &block)
|
12
|
+
@kind = kind
|
13
|
+
@datastore = datastore
|
14
|
+
@filters = []
|
15
|
+
@sorts = []
|
16
|
+
@page = false
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
## Fluent Builder
|
23
|
+
|
24
|
+
def limit(val=nil)
|
25
|
+
set_or_get(:limit, val)
|
26
|
+
end
|
27
|
+
|
28
|
+
def offset(val=nil)
|
29
|
+
set_or_get(:offset, val)
|
30
|
+
end
|
31
|
+
|
32
|
+
def page(page, per_page: 20)
|
33
|
+
@page = page
|
34
|
+
limit(per_page).offset((page - 1) * per_page)
|
35
|
+
end
|
36
|
+
|
37
|
+
def sort(attr, dir=:asc)
|
38
|
+
raise "invalid sort dir #{dir}, must be :asc or :desc" unless [:asc, :desc].include?(dir)
|
39
|
+
@sorts << [attr.to_sym, dir]
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# filter(:name, '=', 'David')
|
44
|
+
# filter(:age, '>', 18)
|
45
|
+
# filter(name: 'David', city: 'Tahoma') # => equivalent to
|
46
|
+
# filter(:name, '=', 'David').filter(:city, '=', 'Tahoma')
|
47
|
+
def filter(attr, op=nil, val=nil)
|
48
|
+
if op.nil?
|
49
|
+
attr.each do |key, value|
|
50
|
+
if value.kind_of?(Enumerable)
|
51
|
+
@filters << [key.to_sym, 'in', value]
|
52
|
+
else
|
53
|
+
@filters << [key.to_sym, '=', value]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
else
|
57
|
+
@filters << [attr.to_sym, op, val]
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
## Query Terminators
|
65
|
+
|
66
|
+
def all
|
67
|
+
with_pagination(@datastore.exec_query(@kind, self))
|
68
|
+
end
|
69
|
+
|
70
|
+
def first
|
71
|
+
limit(1).all.first
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists?
|
75
|
+
!all.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def unpage
|
82
|
+
@page = false
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def set_or_get(name, val)
|
89
|
+
if val.nil?
|
90
|
+
instance_variable_get("@#{name}")
|
91
|
+
else
|
92
|
+
instance_variable_set("@#{name}", val)
|
93
|
+
self
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def with_pagination(results)
|
98
|
+
if @page
|
99
|
+
apply_pagination(results)
|
100
|
+
else
|
101
|
+
results
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def apply_pagination(results)
|
106
|
+
Schron::PaginatedResults.new(results).tap do |r|
|
107
|
+
r.paging.current_page = @page
|
108
|
+
r.paging.previous_page = @page == 1 ? nil : (@page - 1)
|
109
|
+
r.paging.next_page = calculate_next_page
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def calculate_next_page
|
114
|
+
q = dup.unpage
|
115
|
+
q.offset(q.offset + q.limit)
|
116
|
+
.limit(1)
|
117
|
+
q.exists? ?
|
118
|
+
@page + 1 :
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|