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