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