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