hyperion-api 0.0.1.alpha5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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