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
@@ -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
@@ -0,0 +1,7 @@
1
+ module Schron
2
+ class Error < StandardError
3
+ end
4
+
5
+ class OperationDisabledError < Error
6
+ end
7
+ end
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
@@ -0,0 +1,5 @@
1
+ module Schron
2
+ class Paging
3
+ attr_accessor :current_page, :next_page, :previous_page
4
+ end
5
+ end
@@ -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