hyperion-api 0.0.1.alpha1

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.
@@ -0,0 +1,175 @@
1
+ require 'hyperion/query'
2
+ require 'hyperion/filter'
3
+ require 'hyperion/sort'
4
+
5
+ module Hyperion
6
+ class API
7
+
8
+ class << self
9
+
10
+ attr_writer :datastore
11
+
12
+ # Sets the thread-local active datastore
13
+ def datastore=(datastore)
14
+ Thread.current[:datastore] = datastore
15
+ end
16
+
17
+ # Returns the current thread-local datastore instance
18
+ def datastore
19
+ Thread.current[:datastore] || raise('No Datastore installed')
20
+ end
21
+
22
+ # Saves a record. Any additional parameters will get merged onto the record before it is saved.
23
+
24
+ # Hyperion::API.save({:kind => :foo})
25
+ # => {:kind=>"foo", :key=>"<generated key>"}
26
+ # Hyperion::API.save({:kind => :foo}, :value => :bar)
27
+ # => {:kind=>"foo", :value=>:bar, :key=>"<generated key>"}
28
+ def save(record, attrs={})
29
+ save_many([record.merge(attrs || {})]).first
30
+ end
31
+
32
+ # Saves multiple records at once.
33
+ def save_many(records)
34
+ format_records(datastore.save(format_records(records)))
35
+ end
36
+
37
+ # Returns true if the record is new (not saved/doesn't have a :key), false otherwise.
38
+ def new?(record)
39
+ !record.has_key?(:key)
40
+ end
41
+
42
+ # Retrieves the value associated with the given key from the datastore. nil if it doesn't exist.
43
+ def find_by_key(key)
44
+ format_record(datastore.find_by_key(key))
45
+ end
46
+
47
+ # Returns all records of the specified kind that match the filters provided.
48
+ #
49
+ # find_by_kind(:dog) # returns all records with :kind of \"dog\"
50
+ # find_by_kind(:dog, :filters => [[:name, '=', "Fido"]]) # returns all dogs whos name is Fido
51
+ # find_by_kind(:dog, :filters => [[:age, '>', 2], [:age, '<', 5]]) # returns all dogs between the age of 2 and 5 (exclusive)
52
+ # find_by_kind(:dog, :sorts => [[:name, :asc]]) # returns all dogs in alphebetical order of their name
53
+ # find_by_kind(:dog, :sorts => [[:age, :desc], [:name, :asc]]) # returns all dogs ordered from oldest to youngest, and gos of the same age ordered by name
54
+ # find_by_kind(:dog, :limit => 10) # returns upto 10 dogs in undefined order
55
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10) # returns upto the first 10 dogs in alphebetical order of their name
56
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10, :offset => 10) # returns the second set of 10 dogs in alphebetical order of their name
57
+ #
58
+ # Filter operations and acceptable syntax:
59
+ # "=" "eq"
60
+ # "<" "lt"
61
+ # "<=" "lte"
62
+ # ">" "gt"
63
+ # ">=" "gte"
64
+ # "!=" "not"
65
+ # "contains?" "contains" "in?" "in"
66
+ #
67
+ # Sort orders and acceptable syntax:
68
+ # :asc "asc" :ascending "ascending"
69
+ # :desc "desc" :descending "descending"
70
+ def find_by_kind(kind, args={})
71
+ format_records(datastore.find(build_query(kind, args)))
72
+ end
73
+
74
+ # Removes the record stored with the given key. Returns nil no matter what.
75
+ def delete_by_key(key)
76
+ datastore.delete_by_key(key)
77
+ end
78
+
79
+ # Deletes all records of the specified kind that match the filters provided.
80
+ def delete_by_kind(kind, args={})
81
+ datastore.delete(build_query(kind, args))
82
+ end
83
+
84
+ # Counts records of the specified kind that match the filters provided.
85
+ def count_by_kind(kind, args={})
86
+ datastore.count(build_query(kind, args))
87
+ end
88
+
89
+ private
90
+
91
+ def build_query(kind, args)
92
+ kind = format_kind(kind)
93
+ filters = build_filters(args[:filters])
94
+ sorts = build_sorts(args[:sorts])
95
+ Query.new(kind, filters, sorts, args[:limit], args[:offset])
96
+ end
97
+
98
+ def build_filters(filters)
99
+ (filters || []).map do |(field, operator, value)|
100
+ operator = format_operator(operator)
101
+ field = format_field(field)
102
+ Filter.new(field, operator, value)
103
+ end
104
+ end
105
+
106
+ def build_sorts(sorts)
107
+ (sorts || []).map do |(field, order)|
108
+ field = format_field(field)
109
+ order = format_order(order)
110
+ Sort.new(field, order)
111
+ end
112
+ end
113
+
114
+ def format_order(order)
115
+ order.to_sym
116
+ case order
117
+ when :desc, 'desc', 'descending'
118
+ :desc
119
+ when :asc, 'asc', 'ascending'
120
+ :asc
121
+ end
122
+ end
123
+
124
+ def format_operator(operator)
125
+ case operator
126
+ when '=', 'eq'
127
+ '='
128
+ when '!=', 'not'
129
+ '!='
130
+ when '<', 'lt'
131
+ '<'
132
+ when '>', 'gt'
133
+ '>'
134
+ when '<=', 'lte'
135
+ '<='
136
+ when '>=', 'gte'
137
+ '>='
138
+ when 'contains?', 'contains', 'in?', 'in'
139
+ 'contains?'
140
+ end
141
+ end
142
+
143
+ def format_records(records)
144
+ records.map do |record|
145
+ format_record(record)
146
+ end
147
+ end
148
+
149
+ def format_record(record)
150
+ if record
151
+ record = record.reduce({}) do |new_record, (key, value)|
152
+ new_record[snake_case(key.to_s).to_sym] = value
153
+ new_record
154
+ end
155
+ record[:kind] = format_kind(record[:kind])
156
+ record
157
+ end
158
+ end
159
+
160
+ def format_kind(kind)
161
+ snake_case(kind.to_s)
162
+ end
163
+
164
+ def format_field(field)
165
+ snake_case(field.to_s).to_sym
166
+ end
167
+
168
+ def snake_case(str)
169
+ separate_camel_humps = str.gsub(/([a-z0-9])([A-Z])/, '\1 \2').downcase
170
+ separate_camel_humps.gsub(/[ |\-]/, '_')
171
+ end
172
+
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,253 @@
1
+ shared_examples_for 'Datastore' do
2
+
3
+ def api
4
+ Hyperion::API
5
+ end
6
+
7
+ context 'save' do
8
+
9
+ it 'saves a hash and returns it' do
10
+ record = api.save({:kind => 'testing', :name => 'ann'})
11
+ record[:kind].should == 'testing'
12
+ record[:name].should == 'ann'
13
+ end
14
+
15
+ it 'saves an empty record' do
16
+ record = api.save({:kind => 'testing'})
17
+ record[:kind].should == 'testing'
18
+ end
19
+
20
+ it 'assigns a key to new records' do
21
+ record = api.save({:kind => 'testing', :name => 'ann'})
22
+ record[:key].should_not be_nil
23
+ end
24
+
25
+ it 'saves an existing record' do
26
+ record1 = api.save({:kind => 'other_testing', :name => 'ann'})
27
+ record2 = api.save(record1.merge(:name => 'james'))
28
+ record1[:key].should == record2[:key]
29
+ api.find_by_kind('other_testing').length.should == 1
30
+ end
31
+
32
+ it 'saves an existing record to be empty' do
33
+ record1 = api.save({:kind => 'other_testing', :name => 'ann'})
34
+ record2 = record1.dup
35
+ record2.delete(:name)
36
+ record2 = api.save(record2)
37
+ record1[:key].should == record2[:key]
38
+ api.find_by_kind('other_testing').length.should == 1
39
+ end
40
+
41
+ def ten_testing_records(kind = 'testing')
42
+ (1..10).to_a.map do |i|
43
+ {:kind => kind, :name => i.to_s}
44
+ end
45
+ end
46
+
47
+ it 'assigns unique keys to each record' do
48
+ keys = ten_testing_records.map do |record|
49
+ api.save(record)[:key]
50
+ end
51
+ unique_keys = Set.new(keys)
52
+ unique_keys.length.should == 10
53
+ end
54
+
55
+ it 'can save many records' do
56
+ saved_records = api.save_many(ten_testing_records)
57
+ saved_records.length.should == 10
58
+ saved_names = Set.new(saved_records.map { |record| record[:name] })
59
+ found_records = api.find_by_kind('testing')
60
+ found_records.length.should == 10
61
+ found_names = Set.new(found_records.map { |record| record[:name] })
62
+ found_names.should == saved_names
63
+ end
64
+
65
+ end
66
+
67
+ def remove_nils(record)
68
+ record.reduce({}) do |non_nil_record, (field, value)|
69
+ non_nil_record[field] = value unless value.nil?
70
+ non_nil_record
71
+ end
72
+ end
73
+
74
+ context 'find by key' do
75
+ it 'finds by key' do
76
+ record = api.save({:kind => 'testing', :inti => 5})
77
+ remove_nils(api.find_by_key(record[:key])).should == remove_nils(record)
78
+ end
79
+ end
80
+
81
+ context 'find by kind' do
82
+ before :each do
83
+ api.save_many([
84
+ {:kind => 'testing', :inti => 1, :data => 'one' },
85
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
86
+ {:kind => 'testing', :inti => 23, :data => 'twenty3'},
87
+ {:kind => 'testing', :inti => 34, :data => 'thirty4'},
88
+ {:kind => 'testing', :inti => 45, :data => 'forty5' },
89
+ {:kind => 'testing', :inti => 1, :data => 'the one'},
90
+ {:kind => 'testing', :inti => 44, :data => 'forty4' }
91
+ ])
92
+ end
93
+
94
+ it 'filters by the kind' do
95
+ api.save({:kind => 'other_testing', :inti => 5})
96
+ found_records = api.find_by_kind('testing')
97
+ found_records.each do |record|
98
+ record[:kind].should == 'testing'
99
+ end
100
+ end
101
+
102
+ context 'filters' do
103
+
104
+ [
105
+ [[[:inti, '<', 25]], [1, 12, 23], :inti],
106
+ [[[:inti, '<=', 25]], [1, 12, 23], :inti],
107
+ [[[:inti, '>', 25]], [34, 44, 45], :inti],
108
+ [[[:inti, '=', 34]], [34], :inti],
109
+ [[[:inti, '!=', 34]], [1, 12, 23, 44, 45], :inti],
110
+ [[[:inti, 'in', [12, 34]]], [12, 34], :inti],
111
+ [[[:inti, '<', 24], [:inti, '>', 25]], [], :inti],
112
+ [[[:inti, '!=', 12], [:inti, '!=', 23], [:inti, '!=', 34]], [1, 44, 45], :inti],
113
+ [[[:data, '<', 'qux']], ['one', 'forty4', 'forty5'], :data],
114
+ [[[:data, '<=', 'one']], ['one', 'forty4', 'forty5'], :data],
115
+ [[[:data, '>=', 'thirty4']], ['twelve', 'twenty3', 'thirty4'], :data],
116
+ [[[:data, '=', 'one']], ['one'], :data],
117
+ [[[:data, '!=', 'one']], ['the one', 'twelve', 'twenty3', 'thirty4', 'forty4', 'forty5'], :data],
118
+ [[[:data, 'in', ['one', 'twelve']]], ['one', 'twelve'], :data],
119
+ [[[:data, '>', 'qux'], [:data, '<', 'qux']], [], :data],
120
+ [[[:data, '!=', 'one'], [:data, '!=', 'twelve'], [:data, '!=', 'twenty3']], ['the one', 'thirty4', 'forty4', 'forty5'], :data],
121
+ ].each do |filters, result, field|
122
+
123
+ it filters.map(&:to_s).join(', ') do
124
+ found_records = api.find_by_kind('testing', :filters => filters)
125
+ ints = Set.new(found_records.map {|record| record[field]})
126
+ ints.should == Set.new(result)
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+ context 'sorts' do
133
+
134
+ [
135
+ [[[:inti, :asc]], [1, 1, 12, 23, 34, 44, 45], :inti],
136
+ [[[:inti, :desc]], [45, 44, 34, 23, 12, 1, 1], :inti],
137
+ [[[:data, :asc]], [44, 45, 1, 1, 34, 12, 23], :inti],
138
+ [[[:data, :desc]], [23, 12, 34, 1, 1, 45, 44], :inti],
139
+ [[[:inti, :asc], [:data, :asc]], ['one', 'the one', 'twelve', 'twenty3', 'thirty4', 'forty4', 'forty5'], :data],
140
+ [[[:data, :asc], [:inti, :asc]], [44, 45, 1, 1, 34, 12, 23], :inti]
141
+ ].each do |sorts, result, field|
142
+
143
+ it sorts.map(&:to_s).join(', ') do
144
+ found_records = api.find_by_kind('testing', :sorts => sorts)
145
+ ints = found_records.map {|record| record[field]}
146
+ ints.should == result
147
+ end
148
+ end
149
+ end
150
+
151
+ context 'limit and offset' do
152
+ specify 'offset n returns results starting at the nth record' do
153
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :offset => 2)
154
+ ints = found_records.map {|record| record[:inti]}
155
+ ints.should == [12, 23, 34, 44, 45]
156
+ end
157
+
158
+ specify 'limit n takes only the first n records' do
159
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :limit => 2)
160
+ found_records.map {|record| record[:inti]}.should == [1, 1]
161
+
162
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :limit => 1_000_000)
163
+ found_records.map {|record| record[:inti]}.should == [1, 1, 12, 23, 34, 44, 45]
164
+ end
165
+
166
+ [
167
+ [{:limit => 2, :offset => 2}, [[:inti, :asc]], [12, 23]],
168
+ [{:limit => 2, :offset => 4}, [[:inti, :asc]], [34, 44]],
169
+ [{:limit => 2} , [[:inti, :desc]], [45, 44]],
170
+ [{:limit => 2, :offset => 2}, [[:inti, :desc]], [34, 23]],
171
+ [{:limit => 2, :offset => 4}, [[:inti, :desc]], [12, 1]],
172
+ ].each do |constraints, sorts, result|
173
+ example constraints.inspect do
174
+ found_records = api.find_by_kind 'testing', constraints.merge(:sorts => sorts)
175
+ found_records.map { |record| record[:inti] }.should == result
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ context 'delete' do
182
+
183
+ before :each do
184
+ api.save_many([
185
+ {:kind => 'testing', :inti => 1, :data => 'one' },
186
+ {:kind => 'testing', :inti => 12, :data => 'twelve' }
187
+ ])
188
+ end
189
+
190
+ it 'deletes by key' do
191
+ records = api.find_by_kind('testing')
192
+ record_to_delete = records.first
193
+ api.delete_by_key(record_to_delete[:key]).should be_nil
194
+ api.find_by_kind('testing').should_not include(record_to_delete)
195
+ end
196
+
197
+ context 'filters' do
198
+
199
+ [
200
+ [[], []],
201
+ [[[:inti, '=', 1]], [12]],
202
+ [[[:data, '=', 'one']], [12]],
203
+ [[[:inti, '!=', 1]], [1]],
204
+ [[[:inti, '<=', 1]], [12]],
205
+ [[[:inti, '<=', 2]], [12]],
206
+ [[[:inti, '>=', 2]], [1]],
207
+ [[[:inti, '>', 1]], [1]],
208
+ [[[:inti, 'in', [1]]], [12]],
209
+ [[[:inti, 'in', [1, 12]]], []],
210
+ [[[:inti, '=', 2]], [1, 12]]
211
+ ].each do |filters, result|
212
+ it filters.inspect do
213
+ api.delete_by_kind('testing', :filters => filters)
214
+ intis = api.find_by_kind('testing').map {|r| r[:inti]}
215
+ intis.should == result
216
+ end
217
+ end
218
+
219
+ end
220
+ end
221
+
222
+ context 'count' do
223
+
224
+ before :each do
225
+ api.save_many([
226
+ {:kind => 'testing', :inti => 1, :data => 'one' },
227
+ {:kind => 'testing', :inti => 12, :data => 'twelve' }
228
+ ])
229
+ end
230
+
231
+ context 'filters' do
232
+
233
+ [
234
+ [[], 2],
235
+ [[[:inti, '=', 1]], 1],
236
+ [[[:data, '=', 'one']], 1],
237
+ [[[:inti, '!=', 1]], 1],
238
+ [[[:inti, '<=', 1]], 1],
239
+ [[[:inti, '<=', 2]], 1],
240
+ [[[:inti, '>=', 2]], 1],
241
+ [[[:inti, '>', 1]], 1],
242
+ [[[:inti, 'in', [1]]], 1],
243
+ [[[:inti, 'in', [1, 12]]], 2],
244
+ [[[:inti, '=', 2]], 0]
245
+ ].each do |filters, result|
246
+ it filters.inspect do
247
+ api.count_by_kind('testing', :filters => filters).should == result
248
+ end
249
+ end
250
+
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,113 @@
1
+ require 'hyperion/api'
2
+
3
+ module Hyperion
4
+ module Dev
5
+ class Memory
6
+
7
+ def initialize
8
+ @id_counter = 0
9
+ @store = {}
10
+ end
11
+
12
+ def save(records)
13
+ records.map do |record|
14
+ key = API.new?(record) ? generate_key : record[:key]
15
+ record[:key] = key
16
+ store[key] = record
17
+ record
18
+ end
19
+ end
20
+
21
+ def find_by_key(key)
22
+ store[key]
23
+ end
24
+
25
+ def find(query)
26
+ records = store.values
27
+ records = filter_kind(query.kind, records)
28
+ records = apply_filters(query.filters, records)
29
+ records = apply_sorts(query.sorts, records)
30
+ records = apply_offset(query.offset, records)
31
+ records = apply_limit(query.limit, records)
32
+ records
33
+ end
34
+
35
+ def delete_by_key(key)
36
+ store.delete(key)
37
+ nil
38
+ end
39
+
40
+ def delete(query)
41
+ records = find(query)
42
+ store.delete_if do |key, record|
43
+ records.include?(record)
44
+ end
45
+ nil
46
+ end
47
+
48
+ def count(query)
49
+ find(query).length
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :store
55
+
56
+ def filter_kind(kind, records)
57
+ records.select do |record|
58
+ record[:kind] == kind
59
+ end
60
+ end
61
+
62
+ def apply_filters(filters, records)
63
+ records.select do |record|
64
+ filters.all? do |filter|
65
+ value = record[filter.field]
66
+ case filter.operator
67
+ when '<'; value < filter.value
68
+ when '<='; value <= filter.value
69
+ when '>'; value > filter.value
70
+ when '>='; value >= filter.value
71
+ when '='; value == filter.value
72
+ when '!='; value != filter.value
73
+ when 'contains?'; filter.value.include?(value)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def apply_sorts(sorts, records)
80
+ records.sort { |record1, record2| compare_records record1, record2, sorts }
81
+ end
82
+
83
+ def compare_records(record1, record2, sorts)
84
+ sorts.each do |sort|
85
+ result = compare_record record1, record2, sort
86
+ return result if result
87
+ end
88
+ 0
89
+ end
90
+
91
+ def compare_record(record1, record2, sort)
92
+ field1, field2 = record1[sort.field], record2[sort.field]
93
+ field1 == field2 ? nil :
94
+ field1 < field2 && sort.ascending? ? -1 :
95
+ field1 > field2 && sort.descending? ? -1 : 1
96
+ end
97
+
98
+ def generate_key
99
+ @id_counter += 1
100
+ end
101
+
102
+ def apply_offset(offset, records)
103
+ return records unless offset
104
+ records.drop offset
105
+ end
106
+
107
+ def apply_limit(limit, records)
108
+ return records unless limit
109
+ records.take(limit)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,12 @@
1
+ module Hyperion
2
+ class Filter
3
+
4
+ attr_reader :operator, :field, :value
5
+
6
+ def initialize(field, operator, value)
7
+ @operator = operator
8
+ @field = field
9
+ @value = value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Hyperion
2
+ class Query
3
+ attr_reader :kind, :filters, :sorts, :limit, :offset
4
+
5
+ def initialize(kind, filters, sorts, limit, offset)
6
+ @kind = kind
7
+ @filters = filters || []
8
+ @sorts = sorts || []
9
+ @limit = limit
10
+ @offset = offset
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Hyperion
2
+ class Sort
3
+
4
+ attr_reader :field, :order
5
+
6
+ def initialize(field, order)
7
+ @field = field
8
+ @order = order
9
+ end
10
+
11
+ def ascending?
12
+ order == :asc
13
+ end
14
+
15
+ def descending?
16
+ order == :desc
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,203 @@
1
+ require 'hyperion/api'
2
+ require 'hyperion/shared_examples'
3
+ require 'hyperion/fake_ds'
4
+
5
+ describe Hyperion::API do
6
+
7
+ def api
8
+ Hyperion::API
9
+ end
10
+
11
+ context 'datastore' do
12
+ it 'will throw an error if the datastore is called before assignment' do
13
+ expect{ subject.datastore }.to raise_error
14
+ end
15
+ end
16
+
17
+ context 'new?' do
18
+ it 'false if a record exists' do
19
+ api.new?({:key => 1}).should be_false
20
+ end
21
+
22
+ it 'true if a record does not exist' do
23
+ api.new?({}).should be_true
24
+ end
25
+ end
26
+
27
+ context 'with fake datastore' do
28
+ attr_reader :fake_ds
29
+
30
+ before :each do
31
+ @fake_ds = FakeDatastore.new
32
+ api.datastore = @fake_ds
33
+ end
34
+
35
+ context 'save' do
36
+
37
+ it 'saves a record' do
38
+ record = {:kind => 'one'}
39
+ api.save(record)
40
+ api.datastore.saved_records.first.should == record
41
+ end
42
+
43
+ it 'merges the given attrs' do
44
+ api.save({:kind => 'one'}, :attr =>'value')
45
+ api.datastore.saved_records.first.should == {:kind => 'one', :attr => 'value'}
46
+ end
47
+
48
+ it 'handles nil input to attrs' do
49
+ api.save({:kind => 'one'}, nil)
50
+ api.datastore.saved_records.first.should == {:kind => 'one'}
51
+ end
52
+
53
+ context 'record formatting on save' do
54
+ include_examples 'record formatting', lambda { |record|
55
+ Hyperion::API.save(record)
56
+ Hyperion::API.datastore.saved_records.first
57
+ }
58
+ end
59
+
60
+ context 'record formatting on return from datastore' do
61
+ include_examples 'record formatting', lambda {|record|
62
+ Hyperion::API.datastore.returns = [[record]]
63
+ Hyperion::API.save({})
64
+ }
65
+ end
66
+ end
67
+
68
+ context 'save many' do
69
+
70
+ context 'record formatting on save' do
71
+ include_examples 'record formatting', lambda { |record|
72
+ Hyperion::API.save_many([record])
73
+ Hyperion::API.datastore.saved_records.first
74
+ }
75
+ end
76
+
77
+ context 'record formatting on return from datastore' do
78
+ include_examples 'record formatting', lambda { |record|
79
+ Hyperion::API.datastore.returns = [[record]]
80
+ Hyperion::API.save_many([{}]).first
81
+ }
82
+ end
83
+ end
84
+
85
+ context 'find by kind' do
86
+ context 'parses kind' do
87
+ include_examples 'kind formatting', lambda { |kind|
88
+ Hyperion::API.find_by_kind(kind)
89
+ Hyperion::API.datastore.queries.last.kind
90
+ }
91
+ end
92
+
93
+ context 'parses filters' do
94
+ include_examples 'filtering', lambda { |filter|
95
+ Hyperion::API.find_by_kind('kind', :filters => [filter])
96
+ Hyperion::API.datastore.queries.last.filters.first
97
+ }
98
+ end
99
+
100
+ context 'parses sorts' do
101
+
102
+ def do_find(sort)
103
+ api.find_by_kind('kind', :sorts => [sort])
104
+ query = fake_ds.queries.last
105
+ query.sorts.first
106
+ end
107
+
108
+ context 'field' do
109
+ include_examples 'field formatting', lambda { |field|
110
+ Hyperion::API.find_by_kind('kind', :sorts => [[field, 'desc']])
111
+ Hyperion::API.datastore.queries.first.sorts.first.field
112
+ }
113
+ end
114
+
115
+ context 'order' do
116
+ {
117
+ 'desc' => :desc,
118
+ :desc => :desc,
119
+ 'descending' => :desc,
120
+ 'asc' => :asc,
121
+ :asc => :asc,
122
+ 'ascending' => :asc
123
+ }.each_pair do |order, result|
124
+
125
+ it "#{order.inspect} to #{result.inspect}" do
126
+ do_find([:attr, order]).order.should == result
127
+ end
128
+
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'passes limit to the query' do
134
+ api.find_by_kind('kind', :limit => 1)
135
+ fake_ds.queries.first.limit.should == 1
136
+ end
137
+
138
+ it 'passes offset to the query' do
139
+ api.find_by_kind('kind', :offset => 10)
140
+ fake_ds.queries.first.offset.should == 10
141
+ end
142
+
143
+ context 'formats records on return from ds' do
144
+ include_examples 'record formatting', lambda {|record|
145
+ Hyperion::API.datastore.returns = [[record]]
146
+ Hyperion::API.find_by_kind('kind').first
147
+ }
148
+ end
149
+ end
150
+
151
+ context 'delete by kind' do
152
+ context 'parses kind' do
153
+ include_examples 'kind formatting', lambda { |kind|
154
+ Hyperion::API.delete_by_kind(kind)
155
+ Hyperion::API.datastore.queries.last.kind
156
+ }
157
+ end
158
+
159
+ context 'parses filters' do
160
+ include_examples 'filtering', lambda { |filter|
161
+ Hyperion::API.delete_by_kind('kind', :filters => [filter])
162
+ Hyperion::API.datastore.queries.last.filters.first
163
+ }
164
+ end
165
+ end
166
+
167
+ it 'deletes by key' do
168
+ api.delete_by_key('delete_key')
169
+ fake_ds.key_queries.first.should == 'delete_key'
170
+ end
171
+
172
+ context 'count by kind' do
173
+ context 'parses kind' do
174
+ include_examples 'kind formatting', lambda { |kind|
175
+ Hyperion::API.count_by_kind(kind)
176
+ Hyperion::API.datastore.queries.last.kind
177
+ }
178
+ end
179
+
180
+ context 'parses filters' do
181
+ include_examples 'filtering', lambda { |filter|
182
+ Hyperion::API.count_by_kind('kind', :filters => [filter])
183
+ Hyperion::API.datastore.queries.last.filters.first
184
+ }
185
+ end
186
+ end
187
+
188
+ context 'find by key' do
189
+ it 'finds by key' do
190
+ api.find_by_key('key')
191
+ fake_ds.key_queries.first.should == 'key'
192
+ end
193
+
194
+ context 'formats records on return from ds' do
195
+ include_examples 'record formatting', lambda {|record|
196
+ Hyperion::API.datastore.returns = [record]
197
+ Hyperion::API.find_by_key('key')
198
+ }
199
+ end
200
+
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,11 @@
1
+ require 'hyperion/dev/memory'
2
+ require 'hyperion/dev/ds_spec'
3
+
4
+ describe Hyperion::Dev::Memory do
5
+
6
+ before :each do
7
+ Hyperion::API.datastore = Hyperion::Dev::Memory.new
8
+ end
9
+
10
+ it_behaves_like 'Datastore'
11
+ end
@@ -0,0 +1,43 @@
1
+ class FakeDatastore
2
+
3
+ attr_accessor :saved_records, :queries, :key_queries, :returns
4
+
5
+ def initialize
6
+ @saved_records = []
7
+ @returns = []
8
+ @queries = []
9
+ @key_queries = []
10
+ end
11
+
12
+ def save(records)
13
+ @saved_records += records
14
+ returns.shift || []
15
+ end
16
+
17
+ def find_by_key(key)
18
+ @key_queries << key
19
+ returns.shift || nil
20
+ end
21
+
22
+ def find(query)
23
+ @queries << query
24
+ returns.shift || []
25
+ end
26
+
27
+ def delete_by_key(key)
28
+ @key_queries << key
29
+ nil
30
+ end
31
+
32
+ def delete(query)
33
+ @queries << query
34
+ returns.shift || nil
35
+ end
36
+
37
+ def count(query)
38
+ @queries << query
39
+ returns.shift || 0
40
+ end
41
+
42
+ end
43
+
@@ -0,0 +1,93 @@
1
+ shared_examples_for 'kind formatting' do |actor|
2
+ {
3
+ 'one' => 'one',
4
+ :one => 'one',
5
+ :TheKind => 'the_kind',
6
+ 'TheKind' => 'the_kind'
7
+ }.each_pair do |kind, result|
8
+
9
+ it "#{kind} to #{result}" do
10
+ actor.call(kind).should == result
11
+ end
12
+
13
+ end
14
+ end
15
+
16
+ shared_examples_for 'field formatting' do |actor|
17
+
18
+ {
19
+ 'field' => :field,
20
+ 'Field' => :field,
21
+ 'FieldOne' => :field_one,
22
+ 'SomeBigAttr' => :some_big_attr,
23
+ :SomeBigAttr => :some_big_attr,
24
+ 'one-two-three' => :one_two_three,
25
+ 'one two three' => :one_two_three
26
+ }.each_pair do |field, result|
27
+
28
+ it "#{field.inspect} to #{result.inspect}" do
29
+ actor.call(field).should == result
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ shared_examples_for 'record formatting' do |actor|
36
+
37
+ context 'formats kind' do
38
+ include_examples 'kind formatting', lambda { |kind|
39
+ record = actor.call({:kind => kind})
40
+ record[:kind]
41
+ }
42
+ end
43
+
44
+ context 'formats fields' do
45
+ include_examples 'field formatting', lambda { |field|
46
+ record = actor.call({field => 'value'})
47
+ record.delete(:kind)
48
+ record.keys.first
49
+ }
50
+ end
51
+ end
52
+
53
+ shared_examples_for 'filtering' do |actor|
54
+
55
+ context 'field' do
56
+ include_examples 'field formatting', lambda { |field|
57
+ actor.call([field, '=', 0]).field
58
+ }
59
+ end
60
+
61
+ context 'operator' do
62
+
63
+ {
64
+ '=' => '=',
65
+ 'eq' => '=',
66
+ '<' => '<',
67
+ 'lt' => '<',
68
+ '>' => '>',
69
+ 'gt' => '>',
70
+ '<=' => '<=',
71
+ 'lte' => '<=',
72
+ '>=' => '>=',
73
+ 'gte' => '>=',
74
+ '!=' => '!=',
75
+ 'not' => '!=',
76
+ 'contains' => 'contains?',
77
+ 'contains?' => 'contains?',
78
+ 'in?' => 'contains?',
79
+ 'in' => 'contains?',
80
+ }.each_pair do |filter, result|
81
+
82
+ it "#{filter} to #{result}" do
83
+ actor.call([:attr, filter, 0]).operator.should == result
84
+ end
85
+
86
+ end
87
+ end
88
+
89
+ it 'passes the value to the filter' do
90
+ actor.call([:attr, '=', 0]).value.should == 0
91
+ end
92
+ end
93
+
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperion-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha1
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - 8th Light, Inc.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.11.0
30
+ description: A Generic Persistence API for Ruby
31
+ email:
32
+ - myles@8thlight.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/hyperion/dev/memory.rb
38
+ - lib/hyperion/dev/ds_spec.rb
39
+ - lib/hyperion/filter.rb
40
+ - lib/hyperion/api.rb
41
+ - lib/hyperion/sort.rb
42
+ - lib/hyperion/query.rb
43
+ - spec/hyperion/dev/memory_spec.rb
44
+ - spec/hyperion/shared_examples.rb
45
+ - spec/hyperion/api_spec.rb
46
+ - spec/hyperion/fake_ds.rb
47
+ homepage: https://github.com/mylesmegyesi/hyperion-ruby
48
+ licenses:
49
+ - Eclipse Public License
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: 1.8.7
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>'
64
+ - !ruby/object:Gem::Version
65
+ version: 1.3.1
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.24
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: A Generic Persistence API for Ruby
72
+ test_files:
73
+ - spec/hyperion/dev/memory_spec.rb
74
+ - spec/hyperion/shared_examples.rb
75
+ - spec/hyperion/api_spec.rb
76
+ - spec/hyperion/fake_ds.rb