hyperion-api 0.0.1.alpha5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,283 @@
1
+ require 'hyperion/query'
2
+ require 'hyperion/filter'
3
+ require 'hyperion/sort'
4
+ require 'hyperion/util'
5
+ require 'hyperion/format'
6
+
7
+ module Hyperion
8
+
9
+ def self.defentity(kind)
10
+ kind = Format.format_kind(kind)
11
+ kind_spec = KindSpec.new(kind)
12
+ yield(kind_spec)
13
+ save_kind_spec(kind_spec)
14
+ pack(kind.to_sym) {|value| pack_record((value || {}).merge(:kind => kind))}
15
+ unpack(kind.to_sym) {|value| unpack_record((value || {}).merge(:kind => kind))}
16
+ end
17
+
18
+ def self.pack(type, &block)
19
+ @packers ||= {}
20
+ @packers[type] = block
21
+ end
22
+
23
+ def self.unpack(type, &block)
24
+ @unpackers ||= {}
25
+ @unpackers[type] = block
26
+ end
27
+
28
+ # Sets the active datastore
29
+ def self.datastore=(datastore)
30
+ @datastore = datastore
31
+ end
32
+
33
+ # Returns the current datastore instance
34
+ def self.datastore
35
+ Thread.current[:datastore] || @datastore || raise('No Datastore installed')
36
+ end
37
+
38
+ # Assigns the datastore within the given block
39
+ def self.with_datastore(name, opts={})
40
+ Util.bind(:datastore, new_datastore(name, opts)) do
41
+ yield
42
+ end
43
+ end
44
+
45
+ def self.new_datastore(name, opts={})
46
+ begin
47
+ require "hyperion/#{name}"
48
+ rescue LoadError
49
+ raise "Can't find datastore implementation: #{name}"
50
+ end
51
+ ds_klass = Hyperion.const_get(Util.class_name(name.to_s))
52
+ ds_klass.new(opts)
53
+ end
54
+
55
+ # Saves a record. Any additional parameters will get merged onto the record before it is saved.
56
+ #
57
+ # Hyperion.save({:kind => :foo})
58
+ # => {:kind=>"foo", :key=>"<generated key>"}
59
+ # Hyperion.save({:kind => :foo}, :value => :bar)
60
+ # => {:kind=>"foo", :value=>:bar, :key=>"<generated key>"}
61
+ def self.save(record, attrs={})
62
+ save_many([record.merge(attrs || {})]).first
63
+ end
64
+
65
+ # Saves multiple records at once.
66
+ def self.save_many(records)
67
+ unpack_records(datastore.save(pack_records(records)))
68
+ end
69
+
70
+ # Returns true if the record is new (not saved/doesn't have a :key), false otherwise.
71
+ def self.new?(record)
72
+ !record.has_key?(:key)
73
+ end
74
+
75
+ # Retrieves the value associated with the given key from the datastore. nil if it doesn't exist.
76
+ def self.find_by_key(key)
77
+ unpack_record(datastore.find_by_key(key))
78
+ end
79
+
80
+ # Returns all records of the specified kind that match the filters provided.
81
+ #
82
+ # find_by_kind(:dog) # returns all records with :kind of \"dog\"
83
+ # find_by_kind(:dog, :filters => [[:name, '=', "Fido"]]) # returns all dogs whos name is Fido
84
+ # find_by_kind(:dog, :filters => [[:age, '>', 2], [:age, '<', 5]]) # returns all dogs between the age of 2 and 5 (exclusive)
85
+ # find_by_kind(:dog, :sorts => [[:name, :asc]]) # returns all dogs in alphebetical order of their name
86
+ # 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
87
+ # find_by_kind(:dog, :limit => 10) # returns upto 10 dogs in undefined order
88
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10) # returns upto the first 10 dogs in alphebetical order of their name
89
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10, :offset => 10) # returns the second set of 10 dogs in alphebetical order of their name
90
+ #
91
+ # Filter operations and acceptable syntax:
92
+ # "=" "eq"
93
+ # "<" "lt"
94
+ # "<=" "lte"
95
+ # ">" "gt"
96
+ # ">=" "gte"
97
+ # "!=" "not"
98
+ # "contains?" "contains" "in?" "in"
99
+ #
100
+ # Sort orders and acceptable syntax:
101
+ # :asc "asc" :ascending "ascending"
102
+ # :desc "desc" :descending "descending"
103
+ def self.find_by_kind(kind, args={})
104
+ unpack_records(datastore.find(build_query(kind, args)))
105
+ end
106
+
107
+ # Removes the record stored with the given key. Returns nil no matter what.
108
+ def self.delete_by_key(key)
109
+ datastore.delete_by_key(key)
110
+ end
111
+
112
+ # Deletes all records of the specified kind that match the filters provided.
113
+ def self.delete_by_kind(kind, args={})
114
+ datastore.delete(build_query(kind, args))
115
+ end
116
+
117
+ # Counts records of the specified kind that match the filters provided.
118
+ def self.count_by_kind(kind, args={})
119
+ datastore.count(build_query(kind, args))
120
+ end
121
+
122
+ private
123
+
124
+ def self.build_query(kind, args)
125
+ kind = Format.format_kind(kind)
126
+ filters = build_filters(args[:filters])
127
+ sorts = build_sorts(args[:sorts])
128
+ Query.new(kind, filters, sorts, args[:limit], args[:offset])
129
+ end
130
+
131
+ def self.build_filters(filters)
132
+ (filters || []).map do |(field, operator, value)|
133
+ operator = Format.format_operator(operator)
134
+ field = Format.format_field(field)
135
+ Filter.new(field, operator, value)
136
+ end
137
+ end
138
+
139
+ def self.build_sorts(sorts)
140
+ (sorts || []).map do |(field, order)|
141
+ field = Format.format_field(field)
142
+ order = Format.format_order(order)
143
+ Sort.new(field, order)
144
+ end
145
+ end
146
+
147
+ def self.unpack_records(records)
148
+ records.map do |record|
149
+ unpack_record(record)
150
+ end
151
+ end
152
+
153
+ def self.unpack_record(record)
154
+ if record
155
+ create_entity(record) do |field_spec, value|
156
+ field_spec.unpack(value)
157
+ end
158
+ end
159
+ end
160
+
161
+ def self.pack_records(records)
162
+ records.map do |record|
163
+ pack_record(record)
164
+ end
165
+ end
166
+
167
+ def self.pack_record(record)
168
+ if record
169
+ entity = create_entity(record) do |field_spec, value|
170
+ field_spec.pack(value || field_spec.default)
171
+ end
172
+ update_timestamps(entity)
173
+ end
174
+ end
175
+
176
+ def self.packer_for(type)
177
+ @packers[type]
178
+ end
179
+
180
+ def self.unpacker_for(type)
181
+ @unpackers[type]
182
+ end
183
+
184
+ def self.update_timestamps(record)
185
+ new?(record) ? update_created_at(record) : update_updated_at(record)
186
+ end
187
+
188
+ def self.update_updated_at(record)
189
+ spec = kind_spec_for(record[:kind])
190
+ if spec && spec.fields.include?(:updated_at)
191
+ record[:updated_at] = Time.now
192
+ end
193
+ record
194
+ end
195
+
196
+ def self.update_created_at(record)
197
+ spec = kind_spec_for(record[:kind])
198
+ if spec && spec.fields.include?(:created_at)
199
+ record[:created_at] = Time.now
200
+ end
201
+ record
202
+ end
203
+
204
+ def self.create_entity(record)
205
+ record = Format.format_record(record)
206
+ kind = record[:kind]
207
+ spec = kind_spec_for(kind)
208
+ unless spec
209
+ record
210
+ else
211
+ key = record[:key]
212
+ base_record = {:kind => kind}
213
+ base_record[:key] = key if key
214
+ spec.fields.reduce(base_record) do |new_record, (name, spec)|
215
+ new_record[name] = yield(spec, record[name])
216
+ new_record
217
+ end
218
+ end
219
+ end
220
+
221
+ def self.kind_spec_for(kind)
222
+ @kind_specs ||= {}
223
+ @kind_specs[kind]
224
+ end
225
+
226
+ def self.save_kind_spec(kind_spec)
227
+ @kind_specs ||= {}
228
+ @kind_specs[kind_spec.kind] = kind_spec
229
+ end
230
+
231
+ class FieldSpec
232
+
233
+ attr_reader :name, :default
234
+
235
+ def initialize(name, opts={})
236
+ @name = name
237
+ @default = opts[:default]
238
+ @type = opts[:type]
239
+ @packer = opts[:packer]
240
+ @unpacker = opts[:unpacker]
241
+ end
242
+
243
+ def pack(value)
244
+ if @packer && @packer.respond_to?(:call)
245
+ @packer.call(value)
246
+ elsif @type
247
+ type_packer = Hyperion.packer_for(@type)
248
+ type_packer ? type_packer.call(value) : value
249
+ else
250
+ value
251
+ end
252
+ end
253
+
254
+ def unpack(value)
255
+ if @unpacker && @unpacker.respond_to?(:call)
256
+ @unpacker.call(value)
257
+ elsif @type
258
+ type_packer = Hyperion.unpacker_for(@type)
259
+ type_packer ? type_packer.call(value) : value
260
+ else
261
+ value
262
+ end
263
+ end
264
+
265
+ end
266
+
267
+ class KindSpec
268
+
269
+ attr_reader :kind, :fields
270
+
271
+ def initialize(kind)
272
+ @kind = kind
273
+ @fields = {}
274
+ end
275
+
276
+ def field(name, opts={})
277
+ name = Format.format_field(name)
278
+ @fields[name] = FieldSpec.new(name, opts)
279
+ end
280
+
281
+ end
282
+
283
+ end
@@ -1,7 +1,7 @@
1
1
  shared_examples_for 'Datastore' do
2
2
 
3
3
  def api
4
- Hyperion::API
4
+ Hyperion
5
5
  end
6
6
 
7
7
  context 'save' do
@@ -99,6 +99,12 @@ shared_examples_for 'Datastore' do
99
99
  end
100
100
  end
101
101
 
102
+ it "can't filter on old values" do
103
+ record = api.find_by_kind('testing', :filters => [[:inti, '=', 12]]).first
104
+ api.save(record, :inti => 2)
105
+ api.find_by_kind('testing', :filters => [[:inti, '=', 12]]).should == []
106
+ end
107
+
102
108
  context 'filters' do
103
109
 
104
110
  [
@@ -108,6 +114,12 @@ shared_examples_for 'Datastore' do
108
114
  [[[:inti, '=', 34]], [34], :inti],
109
115
  [[[:inti, '!=', 34]], [1, 12, 23, 44, 45], :inti],
110
116
  [[[:inti, 'in', [12, 34]]], [12, 34], :inti],
117
+ [[[:inti, '>', 10], [:inti, '<', 25]], [12, 23], :inti],
118
+ [[[:inti, '<', 25], [:inti, '>', 10]], [12, 23], :inti],
119
+ [[[:inti, '>', 25], [:data, '<', 'thirty4']], [44, 45], :inti],
120
+ [[[:data, '<', 'thirty4'], [:inti, '>', 25]], [44, 45], :inti],
121
+ [[[:inti, '>', 10], [:inti, '<', 25], [:inti, '=', 23]], [23], :inti],
122
+ [[[:inti, '=', 23], [:inti, '>', 10], [:inti, '<', 25]], [23], :inti],
111
123
  [[[:inti, '<', 24], [:inti, '>', 25]], [], :inti],
112
124
  [[[:inti, '!=', 12], [:inti, '!=', 23], [:inti, '!=', 34]], [1, 44, 45], :inti],
113
125
  [[[:data, '<', 'qux']], ['one', 'forty4', 'forty5'], :data],
@@ -212,7 +224,7 @@ shared_examples_for 'Datastore' do
212
224
  it filters.inspect do
213
225
  api.delete_by_kind('testing', :filters => filters)
214
226
  intis = api.find_by_kind('testing').map {|r| r[:inti]}
215
- intis.should == result
227
+ intis.should =~ result
216
228
  end
217
229
  end
218
230
 
@@ -8,5 +8,13 @@ module Hyperion
8
8
  @field = field
9
9
  @value = value
10
10
  end
11
+
12
+ def to_h
13
+ {
14
+ :operator => operator,
15
+ :field => field,
16
+ :value => value
17
+ }
18
+ end
11
19
  end
12
20
  end
@@ -0,0 +1,57 @@
1
+ require 'hyperion/util'
2
+
3
+ module Hyperion
4
+ class Format
5
+
6
+ class << self
7
+
8
+ def format_kind(kind)
9
+ Util.snake_case(kind.to_s)
10
+ end
11
+
12
+ def format_field(field)
13
+ Util.snake_case(field.to_s).to_sym
14
+ end
15
+
16
+ def format_record(record)
17
+ record = record.reduce({}) do |new_record, (field_name, value)|
18
+ new_record[Util.snake_case(field_name.to_s).to_sym] = value
19
+ new_record
20
+ end
21
+ record[:kind] = format_kind(record[:kind])
22
+ record
23
+ end
24
+
25
+ def format_order(order)
26
+ order.to_sym
27
+ case order
28
+ when :desc, 'desc', 'descending'
29
+ :desc
30
+ when :asc, 'asc', 'ascending'
31
+ :asc
32
+ end
33
+ end
34
+
35
+ def format_operator(operator)
36
+ case operator
37
+ when '=', 'eq'
38
+ '='
39
+ when '!=', 'not'
40
+ '!='
41
+ when '<', 'lt'
42
+ '<'
43
+ when '>', 'gt'
44
+ '>'
45
+ when '<=', 'lte'
46
+ '<='
47
+ when '>=', 'gte'
48
+ '>='
49
+ when 'contains?', 'contains', 'in?', 'in'
50
+ 'contains?'
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+
@@ -6,11 +6,11 @@ module Hyperion
6
6
  class << self
7
7
 
8
8
  def encode_key(value)
9
- normalize(Base64.encode64(value))
9
+ normalize(encode(value))
10
10
  end
11
11
 
12
12
  def decode_key(value)
13
- Base64.decode64(denormalize(value))
13
+ decode(denormalize(value))
14
14
  end
15
15
 
16
16
  def compose_key(kind, id=nil)
@@ -22,14 +22,22 @@ module Hyperion
22
22
  decode_key(key).split(/:/).map {|part| decode_key(part)}
23
23
  end
24
24
 
25
- private
26
-
27
25
  def generate_id
28
26
  UUIDTools::UUID.random_create.to_s.gsub(/-/, '')
29
27
  end
30
28
 
29
+ private
30
+
31
+ def encode(str)
32
+ [str].pack('m').tr('+/','-_').gsub("\n",'')
33
+ end
34
+
35
+ def decode(str)
36
+ str.tr('-_','+/').unpack('m')[0]
37
+ end
38
+
31
39
  def normalize(value)
32
- value.gsub(/=/, '').chomp
40
+ value.chomp.gsub(/=/, '')
33
41
  end
34
42
 
35
43
  def denormalize(value)
@@ -1,4 +1,4 @@
1
- require 'hyperion/api'
1
+ require 'hyperion'
2
2
 
3
3
  module Hyperion
4
4
  class Memory
@@ -10,7 +10,7 @@ module Hyperion
10
10
 
11
11
  def save(records)
12
12
  records.map do |record|
13
- key = API.new?(record) ? generate_key : record[:key]
13
+ key = Hyperion.new?(record) ? generate_key : record[:key]
14
14
  record[:key] = key
15
15
  store[key] = record
16
16
  record
@@ -10,5 +10,15 @@ module Hyperion
10
10
  @offset = offset
11
11
  end
12
12
 
13
+ def to_h
14
+ {
15
+ :kind => kind,
16
+ :filters => filters.map(&:to_h),
17
+ :sorts => sorts.map(&:to_h),
18
+ :limit => limit,
19
+ :offset => offset
20
+ }
21
+ end
22
+
13
23
  end
14
24
  end
@@ -15,5 +15,12 @@ module Hyperion
15
15
  def descending?
16
16
  order == :desc
17
17
  end
18
+
19
+ def to_h
20
+ {
21
+ :field => field,
22
+ :order => order
23
+ }
24
+ end
18
25
  end
19
26
  end
@@ -40,7 +40,16 @@ module Hyperion
40
40
  end
41
41
  end
42
42
 
43
- end
43
+ def bind(name, value)
44
+ old_value = Thread.current[name]
45
+ begin
46
+ Thread.current[name] = value
47
+ yield
48
+ ensure
49
+ Thread.current[name] = old_value
50
+ end
51
+ end
44
52
 
53
+ end
45
54
  end
46
55
  end
@@ -36,6 +36,13 @@ describe Hyperion::Key do
36
36
  Hyperion::Key.compose_key(:foo, 1).should == Hyperion::Key.compose_key(:foo, 1)
37
37
  end
38
38
 
39
+ it 'composes and decomposes' do
40
+ kind = "testing"
41
+ id = "BLODQF0Z1DMEfQr7S3eBwfsX4ku"
42
+ key = Hyperion::Key.compose_key(kind, id)
43
+ Hyperion::Key.decompose_key(key).should == [kind, id]
44
+ end
45
+
39
46
  it 'decomposes keys' do
40
47
  key = Hyperion::Key.compose_key(:thing, 1)
41
48
  Hyperion::Key.decompose_key(key).should == ['thing', '1']
@@ -4,7 +4,7 @@ require 'hyperion/dev/ds_spec'
4
4
  describe Hyperion::Memory do
5
5
 
6
6
  around :each do |example|
7
- Hyperion::API.with_datastore(:memory) do
7
+ Hyperion.with_datastore(:memory) do
8
8
  example.run
9
9
  end
10
10
  end
@@ -50,6 +50,197 @@ shared_examples_for 'record formatting' do |actor|
50
50
  end
51
51
  end
52
52
 
53
+ shared_examples_for 'record packing' do |actor|
54
+ include_examples 'record formatting', lambda { |record|
55
+ actor.call(record)
56
+ }
57
+
58
+ Hyperion.defentity(:one_field) do |kind|
59
+ kind.field(:field, :default => 'ABC')
60
+ end
61
+
62
+ it 'only packs fields defined in the entity' do
63
+ result = actor.call(:kind => :one_field, :field => 'value', :foo => 'bar')
64
+ result.should == {:kind => 'one_field', :field => 'value'}
65
+ end
66
+
67
+ it 'packs the key field if not defined' do
68
+ result = actor.call(:kind => :one_field, :key => '1234', :field => 'value', :foo => 'bar')
69
+ result.should == {:kind => 'one_field', :key => '1234', :field => 'value'}
70
+ end
71
+
72
+ Hyperion.defentity(:two_fields) do |kind|
73
+ kind.field(:field1, :default => 'ABC')
74
+ kind.field(:field2, :default => 'CBD')
75
+ end
76
+
77
+ it 'applies defaults' do
78
+ actor.call(:kind => :two_fields).should == {:kind => 'two_fields', :field1 => 'ABC', :field2 => 'CBD'}
79
+ actor.call(:kind => :two_fields, :field1 => 'john').should == {:kind => 'two_fields', :field1 => 'john', :field2 => 'CBD'}
80
+ end
81
+
82
+ Hyperion.pack(Integer) do |value|
83
+ value ? value.to_i : value
84
+ end
85
+
86
+ Hyperion.pack(:up) do |value|
87
+ value ? value.upcase : value
88
+ end
89
+
90
+ Hyperion.pack(:down) do |value|
91
+ value ? value.downcase : value
92
+ end
93
+
94
+ Hyperion.defentity('nestedType') do |kind|
95
+ kind.field(:thingy, :type => Integer, :default => '2')
96
+ kind.field(:upped, :type => :up, :default => 'asdf')
97
+ end
98
+
99
+ Hyperion.defentity(:packable) do |kind|
100
+ kind.field(:widget, :type => Integer)
101
+ kind.field(:downed, :type => :down)
102
+ kind.field(:thing, :type => :nested_type)
103
+ kind.field(:bauble, :packer => lambda {|value| value ? value.reverse : value})
104
+ kind.field(:bad_packer, :packer => true)
105
+ kind.field(:two_packer, :type => Integer, :packer => lambda {|value| value ? value.reverse : value})
106
+ end
107
+
108
+ it 'packs the given type' do
109
+ result = actor.call(:kind => :packable, :downed => 'ABC', :widget => '1')
110
+ result[:downed].should == 'abc'
111
+ result[:widget].should == 1
112
+ end
113
+
114
+ it 'packs nested types' do
115
+ result = actor.call(:kind => :packable, :downed => 'ABC', :widget => '1')
116
+ result[:thing].should == {:kind => 'nested_type', :thingy => 2, :upped => 'ASDF'}
117
+ end
118
+
119
+ it 'packs nested types and merges existing data' do
120
+ result = actor.call(:kind => :packable, :downed => 'ABC', :widget => '1', :thing => {:upped => 'FDAS'})
121
+ result[:thing].should == {:kind => 'nested_type', :thingy => 2, :upped => 'FDAS'}
122
+ end
123
+
124
+ it 'packs with custom callables' do
125
+ result = actor.call(:kind => :packable, :bauble => 'cba')
126
+ result[:bauble].should == 'abc'
127
+ end
128
+
129
+ it 'custom callable must respond to `call`' do
130
+ result = actor.call(:kind => :packable, :bad_packer => 'thing')
131
+ result[:bad_packer].should == 'thing'
132
+ end
133
+
134
+ it 'prefers the custom packer over the type packer' do
135
+ result = actor.call(:kind => :packable, :two_packer => 'thing')
136
+ result[:two_packer].should == 'gniht'
137
+ end
138
+
139
+ context 'Timestamps' do
140
+
141
+ Hyperion.defentity(:with_time) do |kind|
142
+ kind.field(:created_at)
143
+ kind.field(:updated_at)
144
+ end
145
+
146
+ Hyperion.defentity(:without_time) do |kind|
147
+ end
148
+
149
+ before :each do
150
+ @now = mock(:now)
151
+ Time.stub(:now).and_return(@now)
152
+ end
153
+
154
+ it 'auto populates created_at if it exists and if the record is new' do
155
+ old_time = mock(:old_time)
156
+ actor.call(:kind => :without_time).should == {:kind => 'without_time'}
157
+ actor.call(:kind => :with_time, :key => '1234', :created_at => old_time)[:created_at].should == old_time
158
+ actor.call(:kind => :with_time)[:created_at].should == @now
159
+ end
160
+
161
+ it 'auto populates updated_at if it exists and if the record is not new' do
162
+ old_time = mock(:old_time)
163
+ actor.call(:kind => :without_time).should == {:kind => 'without_time'}
164
+ result = actor.call(:kind => :with_time, :key => '1234', :created_at => old_time, :updated_at => old_time)
165
+ result[:created_at].should == old_time
166
+ result[:updated_at].should == @now
167
+ end
168
+ end
169
+ end
170
+
171
+ shared_examples_for 'record unpacking' do |actor|
172
+ include_examples 'record formatting', lambda { |record|
173
+ actor.call(record)
174
+ }
175
+
176
+ it 'only unpacks defined fields' do
177
+ result = actor.call(:kind => :one_field, :field => 'value', :foo => 'bar')
178
+ result.should == {:kind=>"one_field", :field=>"value"}
179
+ end
180
+
181
+ it 'unpacks the key field if not defined' do
182
+ result = actor.call(:kind => :one_field, :key => '1234', :field => 'value', :foo => 'bar')
183
+ result.should == {:kind=>"one_field", :key => '1234', :field=>"value"}
184
+ end
185
+
186
+ Hyperion.unpack(Integer) do |value|
187
+ value ? value.to_i : value
188
+ end
189
+
190
+ Hyperion.unpack(:up) do |value|
191
+ value ? value.upcase : value
192
+ end
193
+
194
+ Hyperion.unpack(:down) do |value|
195
+ value ? value.downcase : value
196
+ end
197
+
198
+ Hyperion.defentity('nested') do |kind|
199
+ kind.field(:thingy, :type => Integer, :default => '2')
200
+ kind.field(:upped, :type => :up, :default => 'asdf')
201
+ end
202
+
203
+ Hyperion.defentity(:unpackable) do |kind|
204
+ kind.field(:widget, :type => Integer)
205
+ kind.field(:downed, :type => :down)
206
+ kind.field(:thing, :type => :nested_type)
207
+ kind.field(:bauble, :unpacker => lambda {|value| value ? value.reverse : value})
208
+ kind.field(:bad_packer, :unpacker => true)
209
+ kind.field(:two_packer, :type => Integer, :unpacker => lambda {|value| value ? value.reverse : value})
210
+ end
211
+
212
+ it 'unpacks the given type' do
213
+ result = actor.call(:kind => :unpackable, :downed => 'ABC', :widget => '1')
214
+ result[:downed].should == 'abc'
215
+ result[:widget].should == 1
216
+ end
217
+
218
+ it 'unpacks nested types' do
219
+ result = actor.call(:kind => :unpackable, :downed => 'ABC', :widget => '1')
220
+ result[:thing].should == {:kind => 'nested_type', :thingy => nil, :upped => nil}
221
+ end
222
+
223
+ it 'unpacks nested types and merges existing data' do
224
+ result = actor.call(:kind => :unpackable, :downed => 'ABC', :widget => '1', :thing => {:upped => 'FDAS'})
225
+ result[:thing].should == {:kind => 'nested_type', :thingy => nil, :upped => 'FDAS'}
226
+ end
227
+
228
+ it 'unpacks with custom callables' do
229
+ result = actor.call(:kind => :unpackable, :bauble => 'cba')
230
+ result[:bauble].should == 'abc'
231
+ end
232
+
233
+ it 'custom callable must respond to `call`' do
234
+ result = actor.call(:kind => :unpackable, :bad_packer => 'thing')
235
+ result[:bad_packer].should == 'thing'
236
+ end
237
+
238
+ it 'prefers the custom packer over the type packer' do
239
+ result = actor.call(:kind => :unpackable, :two_packer => 'thing')
240
+ result[:two_packer].should == 'gniht'
241
+ end
242
+ end
243
+
53
244
  shared_examples_for 'filtering' do |actor|
54
245
 
55
246
  context 'field' do
@@ -51,4 +51,45 @@ describe Hyperion::Util do
51
51
  util.class_name('').should == ''
52
52
  util.class_name(nil).should == nil
53
53
  end
54
+
55
+ context 'binding' do
56
+ it 'assigns the thread local var within the block' do
57
+ called = false
58
+ util.bind(:thing, 1) do
59
+ called = true
60
+ Thread.current[:thing].should == 1
61
+ end
62
+ called.should be_true
63
+ end
64
+
65
+ it 'reassigns the previous value' do
66
+ Thread.current[:thing].should == nil
67
+ util.bind(:thing, 1) do
68
+ util.bind(:thing, 2) do
69
+ Thread.current[:thing].should == 2
70
+ end
71
+ Thread.current[:thing].should == 1
72
+ end
73
+ Thread.current[:thing].should == nil
74
+ end
75
+
76
+ it 'reassigns when an exception is thrown' do
77
+ Thread.current[:thing].should == nil
78
+ util.bind(:thing, 1) do
79
+ expect {
80
+ util.bind(:thing, 2) do
81
+ raise 'my exception'
82
+ end
83
+ }.to raise_error('my exception')
84
+ Thread.current[:thing].should == 1
85
+ end
86
+ Thread.current[:thing].should == nil
87
+ end
88
+
89
+ it 'return the result of the block' do
90
+ util.bind(:thing, 1) do
91
+ :return
92
+ end.should == :return
93
+ end
94
+ end
54
95
  end
@@ -1,11 +1,11 @@
1
- require 'hyperion/api'
1
+ require 'hyperion'
2
2
  require 'hyperion/shared_examples'
3
3
  require 'hyperion/fake_ds'
4
4
 
5
- describe Hyperion::API do
5
+ describe Hyperion do
6
6
 
7
7
  def api
8
- Hyperion::API
8
+ Hyperion
9
9
  end
10
10
 
11
11
  context 'datastore' do
@@ -13,11 +13,27 @@ describe Hyperion::API do
13
13
  expect{ subject.datastore }.to raise_error
14
14
  end
15
15
 
16
- it 'assigns datastore and returns the result' do
16
+ it 'assigns the datastore with brute force' do
17
+ api.datastore = :something
18
+ api.datastore.should == :something
19
+ api.datastore = nil
20
+ end
21
+
22
+ it 'assigns datastore with elegance and returns the result' do
17
23
  api.with_datastore(:memory) do
24
+ api.datastore.should be_a(Hyperion::Memory)
18
25
  :return
19
26
  end.should == :return
20
27
  end
28
+
29
+ it 'prefers the thread-local datastore over the global datastore' do
30
+ api.datastore = :something_else
31
+ api.with_datastore(:memory) do
32
+ api.datastore.should be_a(Hyperion::Memory)
33
+ end
34
+ api.datastore = :something_else
35
+ api.datastore = nil
36
+ end
21
37
  end
22
38
 
23
39
  context 'factory' do
@@ -41,10 +57,9 @@ describe Hyperion::API do
41
57
  end
42
58
 
43
59
  context 'with fake datastore' do
44
- attr_reader :fake_ds
45
60
 
46
61
  def fake_ds
47
- Thread.current[:datastore]
62
+ api.datastore
48
63
  end
49
64
 
50
65
  around :each do |example|
@@ -71,34 +86,34 @@ describe Hyperion::API do
71
86
  api.datastore.saved_records.first.should == {:kind => 'one'}
72
87
  end
73
88
 
74
- context 'record formatting on save' do
75
- include_examples 'record formatting', lambda { |record|
76
- Hyperion::API.save(record)
77
- Hyperion::API.datastore.saved_records.first
89
+ context 'record packing on save' do
90
+ include_examples 'record packing', lambda { |record|
91
+ Hyperion.save(record)
92
+ Hyperion.datastore.saved_records.last
78
93
  }
79
94
  end
80
95
 
81
- context 'record formatting on return from datastore' do
82
- include_examples 'record formatting', lambda {|record|
83
- Hyperion::API.datastore.returns = [[record]]
84
- Hyperion::API.save({})
96
+ context 'record unpacking on return from datastore' do
97
+ include_examples 'record unpacking', lambda {|record|
98
+ Hyperion.datastore.returns = [[record]]
99
+ Hyperion.save({})
85
100
  }
86
101
  end
87
102
  end
88
103
 
89
104
  context 'save many' do
90
105
 
91
- context 'record formatting on save' do
92
- include_examples 'record formatting', lambda { |record|
93
- Hyperion::API.save_many([record])
94
- Hyperion::API.datastore.saved_records.first
106
+ context 'record packing on save' do
107
+ include_examples 'record packing', lambda { |record|
108
+ Hyperion.save_many([record])
109
+ Hyperion.datastore.saved_records.last
95
110
  }
96
111
  end
97
112
 
98
- context 'record formatting on return from datastore' do
99
- include_examples 'record formatting', lambda { |record|
100
- Hyperion::API.datastore.returns = [[record]]
101
- Hyperion::API.save_many([{}]).first
113
+ context 'record unpacking on return from datastore' do
114
+ include_examples 'record unpacking', lambda { |record|
115
+ Hyperion.datastore.returns = [[record]]
116
+ Hyperion.save_many([{}]).last
102
117
  }
103
118
  end
104
119
  end
@@ -106,15 +121,15 @@ describe Hyperion::API do
106
121
  context 'find by kind' do
107
122
  context 'parses kind' do
108
123
  include_examples 'kind formatting', lambda { |kind|
109
- Hyperion::API.find_by_kind(kind)
110
- Hyperion::API.datastore.queries.last.kind
124
+ Hyperion.find_by_kind(kind)
125
+ Hyperion.datastore.queries.last.kind
111
126
  }
112
127
  end
113
128
 
114
129
  context 'parses filters' do
115
130
  include_examples 'filtering', lambda { |filter|
116
- Hyperion::API.find_by_kind('kind', :filters => [filter])
117
- Hyperion::API.datastore.queries.last.filters.first
131
+ Hyperion.find_by_kind('kind', :filters => [filter])
132
+ Hyperion.datastore.queries.last.filters.first
118
133
  }
119
134
  end
120
135
 
@@ -128,8 +143,8 @@ describe Hyperion::API do
128
143
 
129
144
  context 'field' do
130
145
  include_examples 'field formatting', lambda { |field|
131
- Hyperion::API.find_by_kind('kind', :sorts => [[field, 'desc']])
132
- Hyperion::API.datastore.queries.first.sorts.first.field
146
+ Hyperion.find_by_kind('kind', :sorts => [[field, 'desc']])
147
+ Hyperion.datastore.queries.first.sorts.first.field
133
148
  }
134
149
  end
135
150
 
@@ -162,9 +177,9 @@ describe Hyperion::API do
162
177
  end
163
178
 
164
179
  context 'formats records on return from ds' do
165
- include_examples 'record formatting', lambda {|record|
166
- Hyperion::API.datastore.returns = [[record]]
167
- Hyperion::API.find_by_kind('kind').first
180
+ include_examples 'record unpacking', lambda {|record|
181
+ Hyperion.datastore.returns = [[record]]
182
+ Hyperion.find_by_kind('kind').first
168
183
  }
169
184
  end
170
185
  end
@@ -172,15 +187,15 @@ describe Hyperion::API do
172
187
  context 'delete by kind' do
173
188
  context 'parses kind' do
174
189
  include_examples 'kind formatting', lambda { |kind|
175
- Hyperion::API.delete_by_kind(kind)
176
- Hyperion::API.datastore.queries.last.kind
190
+ Hyperion.delete_by_kind(kind)
191
+ Hyperion.datastore.queries.last.kind
177
192
  }
178
193
  end
179
194
 
180
195
  context 'parses filters' do
181
196
  include_examples 'filtering', lambda { |filter|
182
- Hyperion::API.delete_by_kind('kind', :filters => [filter])
183
- Hyperion::API.datastore.queries.last.filters.first
197
+ Hyperion.delete_by_kind('kind', :filters => [filter])
198
+ Hyperion.datastore.queries.last.filters.first
184
199
  }
185
200
  end
186
201
  end
@@ -193,15 +208,15 @@ describe Hyperion::API do
193
208
  context 'count by kind' do
194
209
  context 'parses kind' do
195
210
  include_examples 'kind formatting', lambda { |kind|
196
- Hyperion::API.count_by_kind(kind)
197
- Hyperion::API.datastore.queries.last.kind
211
+ Hyperion.count_by_kind(kind)
212
+ Hyperion.datastore.queries.last.kind
198
213
  }
199
214
  end
200
215
 
201
216
  context 'parses filters' do
202
217
  include_examples 'filtering', lambda { |filter|
203
- Hyperion::API.count_by_kind('kind', :filters => [filter])
204
- Hyperion::API.datastore.queries.last.filters.first
218
+ Hyperion.count_by_kind('kind', :filters => [filter])
219
+ Hyperion.datastore.queries.last.filters.first
205
220
  }
206
221
  end
207
222
  end
@@ -213,9 +228,9 @@ describe Hyperion::API do
213
228
  end
214
229
 
215
230
  context 'formats records on return from ds' do
216
- include_examples 'record formatting', lambda {|record|
217
- Hyperion::API.datastore.returns = [record]
218
- Hyperion::API.find_by_key('key')
231
+ include_examples 'record unpacking', lambda {|record|
232
+ Hyperion.datastore.returns = [record]
233
+ Hyperion.find_by_key('key')
219
234
  }
220
235
  end
221
236
 
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperion-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha5
5
- prerelease: 6
4
+ version: 0.1.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
- - 8th Light, Inc.
8
+ - Myles Megyesi
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-18 00:00:00.000000000 Z
12
+ date: 2012-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -46,24 +46,24 @@ dependencies:
46
46
  description: A Generic Persistence API for Ruby
47
47
  email:
48
48
  - myles@8thlight.com
49
- - skim@8thlight.com
50
49
  executables: []
51
50
  extensions: []
52
51
  extra_rdoc_files: []
53
52
  files:
54
53
  - lib/hyperion/dev/ds_spec.rb
54
+ - lib/hyperion/format.rb
55
55
  - lib/hyperion/util.rb
56
56
  - lib/hyperion/filter.rb
57
- - lib/hyperion/api.rb
58
57
  - lib/hyperion/key.rb
59
58
  - lib/hyperion/memory.rb
60
59
  - lib/hyperion/sort.rb
61
60
  - lib/hyperion/query.rb
61
+ - lib/hyperion.rb
62
+ - spec/hyperion_spec.rb
62
63
  - spec/hyperion/util_spec.rb
63
64
  - spec/hyperion/key_spec.rb
64
65
  - spec/hyperion/shared_examples.rb
65
66
  - spec/hyperion/memory_spec.rb
66
- - spec/hyperion/api_spec.rb
67
67
  - spec/hyperion/fake_ds.rb
68
68
  homepage: https://github.com/mylesmegyesi/hyperion-ruby
69
69
  licenses:
@@ -81,9 +81,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
81
81
  required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  none: false
83
83
  requirements:
84
- - - ! '>'
84
+ - - ! '>='
85
85
  - !ruby/object:Gem::Version
86
- version: 1.3.1
86
+ version: '0'
87
87
  requirements: []
88
88
  rubyforge_project:
89
89
  rubygems_version: 1.8.24
@@ -91,9 +91,9 @@ signing_key:
91
91
  specification_version: 3
92
92
  summary: A Generic Persistence API for Ruby
93
93
  test_files:
94
+ - spec/hyperion_spec.rb
94
95
  - spec/hyperion/util_spec.rb
95
96
  - spec/hyperion/key_spec.rb
96
97
  - spec/hyperion/shared_examples.rb
97
98
  - spec/hyperion/memory_spec.rb
98
- - spec/hyperion/api_spec.rb
99
99
  - spec/hyperion/fake_ds.rb
@@ -1,189 +0,0 @@
1
- require 'hyperion/query'
2
- require 'hyperion/filter'
3
- require 'hyperion/sort'
4
- require 'hyperion/util'
5
-
6
- module Hyperion
7
- class API
8
-
9
- class << self
10
-
11
- attr_writer :datastore
12
-
13
- # Sets the thread-local active datastore
14
- def datastore=(datastore)
15
- Thread.current[:datastore] = datastore
16
- end
17
-
18
- # Returns the current thread-local datastore instance
19
- def datastore
20
- Thread.current[:datastore] || raise('No Datastore installed')
21
- end
22
-
23
- # Assigns the datastore within the given block
24
- def with_datastore(name, opts={})
25
- self.datastore = new_datastore(name, opts)
26
- result = yield
27
- self.datastore = nil
28
- result
29
- end
30
-
31
- def new_datastore(name, opts={})
32
- begin
33
- require "hyperion/#{name}"
34
- rescue LoadError
35
- raise "Can't find datastore implementation: #{name}"
36
- end
37
- ds_klass = Hyperion.const_get(Util.class_name(name.to_s))
38
- ds_klass.new(opts)
39
- end
40
-
41
- # Saves a record. Any additional parameters will get merged onto the record before it is saved.
42
-
43
- # Hyperion::API.save({:kind => :foo})
44
- # => {:kind=>"foo", :key=>"<generated key>"}
45
- # Hyperion::API.save({:kind => :foo}, :value => :bar)
46
- # => {:kind=>"foo", :value=>:bar, :key=>"<generated key>"}
47
- def save(record, attrs={})
48
- save_many([record.merge(attrs || {})]).first
49
- end
50
-
51
- # Saves multiple records at once.
52
- def save_many(records)
53
- format_records(datastore.save(format_records(records)))
54
- end
55
-
56
- # Returns true if the record is new (not saved/doesn't have a :key), false otherwise.
57
- def new?(record)
58
- !record.has_key?(:key)
59
- end
60
-
61
- # Retrieves the value associated with the given key from the datastore. nil if it doesn't exist.
62
- def find_by_key(key)
63
- format_record(datastore.find_by_key(key))
64
- end
65
-
66
- # Returns all records of the specified kind that match the filters provided.
67
- #
68
- # find_by_kind(:dog) # returns all records with :kind of \"dog\"
69
- # find_by_kind(:dog, :filters => [[:name, '=', "Fido"]]) # returns all dogs whos name is Fido
70
- # find_by_kind(:dog, :filters => [[:age, '>', 2], [:age, '<', 5]]) # returns all dogs between the age of 2 and 5 (exclusive)
71
- # find_by_kind(:dog, :sorts => [[:name, :asc]]) # returns all dogs in alphebetical order of their name
72
- # 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
73
- # find_by_kind(:dog, :limit => 10) # returns upto 10 dogs in undefined order
74
- # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10) # returns upto the first 10 dogs in alphebetical order of their name
75
- # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10, :offset => 10) # returns the second set of 10 dogs in alphebetical order of their name
76
- #
77
- # Filter operations and acceptable syntax:
78
- # "=" "eq"
79
- # "<" "lt"
80
- # "<=" "lte"
81
- # ">" "gt"
82
- # ">=" "gte"
83
- # "!=" "not"
84
- # "contains?" "contains" "in?" "in"
85
- #
86
- # Sort orders and acceptable syntax:
87
- # :asc "asc" :ascending "ascending"
88
- # :desc "desc" :descending "descending"
89
- def find_by_kind(kind, args={})
90
- format_records(datastore.find(build_query(kind, args)))
91
- end
92
-
93
- # Removes the record stored with the given key. Returns nil no matter what.
94
- def delete_by_key(key)
95
- datastore.delete_by_key(key)
96
- end
97
-
98
- # Deletes all records of the specified kind that match the filters provided.
99
- def delete_by_kind(kind, args={})
100
- datastore.delete(build_query(kind, args))
101
- end
102
-
103
- # Counts records of the specified kind that match the filters provided.
104
- def count_by_kind(kind, args={})
105
- datastore.count(build_query(kind, args))
106
- end
107
-
108
- private
109
-
110
- def build_query(kind, args)
111
- kind = format_kind(kind)
112
- filters = build_filters(args[:filters])
113
- sorts = build_sorts(args[:sorts])
114
- Query.new(kind, filters, sorts, args[:limit], args[:offset])
115
- end
116
-
117
- def build_filters(filters)
118
- (filters || []).map do |(field, operator, value)|
119
- operator = format_operator(operator)
120
- field = format_field(field)
121
- Filter.new(field, operator, value)
122
- end
123
- end
124
-
125
- def build_sorts(sorts)
126
- (sorts || []).map do |(field, order)|
127
- field = format_field(field)
128
- order = format_order(order)
129
- Sort.new(field, order)
130
- end
131
- end
132
-
133
- def format_order(order)
134
- order.to_sym
135
- case order
136
- when :desc, 'desc', 'descending'
137
- :desc
138
- when :asc, 'asc', 'ascending'
139
- :asc
140
- end
141
- end
142
-
143
- def format_operator(operator)
144
- case operator
145
- when '=', 'eq'
146
- '='
147
- when '!=', 'not'
148
- '!='
149
- when '<', 'lt'
150
- '<'
151
- when '>', 'gt'
152
- '>'
153
- when '<=', 'lte'
154
- '<='
155
- when '>=', 'gte'
156
- '>='
157
- when 'contains?', 'contains', 'in?', 'in'
158
- 'contains?'
159
- end
160
- end
161
-
162
- def format_records(records)
163
- records.map do |record|
164
- format_record(record)
165
- end
166
- end
167
-
168
- def format_record(record)
169
- if record
170
- record = record.reduce({}) do |new_record, (key, value)|
171
- new_record[Util.snake_case(key.to_s).to_sym] = value
172
- new_record
173
- end
174
- record[:kind] = format_kind(record[:kind])
175
- record
176
- end
177
- end
178
-
179
- def format_kind(kind)
180
- Util.snake_case(kind.to_s)
181
- end
182
-
183
- def format_field(field)
184
- Util.snake_case(field.to_s).to_sym
185
- end
186
-
187
- end
188
- end
189
- end