hyperion-api 0.0.1.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/hyperion/api.rb +175 -0
- data/lib/hyperion/dev/ds_spec.rb +253 -0
- data/lib/hyperion/dev/memory.rb +113 -0
- data/lib/hyperion/filter.rb +12 -0
- data/lib/hyperion/query.rb +14 -0
- data/lib/hyperion/sort.rb +19 -0
- data/spec/hyperion/api_spec.rb +203 -0
- data/spec/hyperion/dev/memory_spec.rb +11 -0
- data/spec/hyperion/fake_ds.rb +43 -0
- data/spec/hyperion/shared_examples.rb +93 -0
- metadata +76 -0
data/lib/hyperion/api.rb
ADDED
@@ -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,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,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,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
|