endymion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7111de9207c18485a31aece27f1ed4f68ef6399d
4
+ data.tar.gz: 6643fae41c6caafee6a1192fa2ebaa5ef23d37e1
5
+ SHA512:
6
+ metadata.gz: a9857cb58fba27edae36ebddd1b48e0dd27001752bde1dac7c74c0bbc4c2935f64d3dcaa60b5a86afd29b0a763e2725bd919db9959c7a4020a17a7ea96c16510
7
+ data.tar.gz: ee254058fd74cb6a91c59526f09ac5fde80aa95afbc20cd5b103135efe59f6f2c0957ed07545f96bdf998bff2ea997fd21290fe6ae81ba55f5d536af4ee688eb
data/lib/endymion.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'endymion/api'
2
+ module Endymion
3
+
4
+ def self.new(datastore_name, datastore_opts={})
5
+ Endymion::API.new(new_datastore(datastore_name, datastore_opts))
6
+ end
7
+
8
+ def self.new_datastore(name, opts={})
9
+ begin
10
+ require "endymion/#{name}"
11
+ rescue LoadError
12
+ raise "Can't find datastore implementation: #{name}"
13
+ end
14
+ ds_klass = Endymion.const_get(Util.class_name(name.to_s))
15
+ ds_klass.new(opts)
16
+ end
17
+
18
+ def self.new?(record)
19
+ !record.has_key?(:key)
20
+ end
21
+ end
@@ -0,0 +1,284 @@
1
+ require 'endymion/format'
2
+ require 'endymion/field_spec'
3
+ require 'endymion/kind_spec'
4
+ require 'endymion/query'
5
+ require 'endymion/filter'
6
+ require 'endymion/sort'
7
+
8
+ module Endymion
9
+ class API
10
+
11
+ attr_reader :datastore
12
+
13
+ def initialize(datastore)
14
+ @datastore = datastore
15
+ end
16
+
17
+ def defentity(kind)
18
+ kind = Format.format_kind(kind)
19
+ kind_spec = KindSpec.new(kind, self)
20
+ yield(kind_spec)
21
+ save_kind_spec(kind_spec)
22
+ pack(kind.to_sym) {|value| pack_record((value || {}).merge(:kind => kind))}
23
+ unpack(kind.to_sym) {|value| unpack_record((value || {}).merge(:kind => kind))}
24
+ end
25
+
26
+ def pack(type, &block)
27
+ packers[type] = block
28
+ end
29
+
30
+ def packer_defined?(type)
31
+ packers.has_key?(type)
32
+ end
33
+
34
+ def unpack(type, &block)
35
+ unpackers[type] = block
36
+ end
37
+
38
+ def unpacker_defined?(type)
39
+ unpackers.has_key?(type)
40
+ end
41
+
42
+ # Assigns the datastore within the given block
43
+ def with_datastore(name, opts={})
44
+ old_datastore = @datastore
45
+ @datastore = Endymion.new_datastore(name, opts)
46
+ begin
47
+ yield
48
+ rescue
49
+ @datastore = old_datastore
50
+ raise
51
+ end
52
+ end
53
+
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 save(record, attrs={})
62
+ save_many([record.merge(attrs || {})]).first
63
+ end
64
+
65
+ # Saves multiple records at once.
66
+ def save_many(records)
67
+ unpack_records(datastore.save(pack_records(records)))
68
+ end
69
+
70
+ # Creates a record. While save delegates to a create or update based on the result
71
+ # of new?, this always creates. This allows you to set the key explicitly
72
+ def create(record)
73
+ create_many([record]).first
74
+ end
75
+
76
+ # Create many records at once
77
+ def create_many(records)
78
+ unpack_records(datastore.create(pack_records(records)))
79
+ end
80
+
81
+ # Returns true if the record is new (not saved/doesn't have a :key), false otherwise.
82
+ def new?(record)
83
+ !record.has_key?(:key)
84
+ end
85
+
86
+ # Retrieves the value associated with the given key from the datastore. nil if it doesn't exist.
87
+ def find_by_key(kind, key)
88
+ unpack_record(datastore.find_by_key(kind, key))
89
+ end
90
+
91
+ # Returns all records of the specified kind that match the filters provided.
92
+ #
93
+ # find_by_kind(:dog) # returns all records with :kind of \"dog\"
94
+ # find_by_kind(:dog, :filters => [[:name, '=', "Fido"]]) # returns all dogs whos name is Fido
95
+ # find_by_kind(:dog, :filters => [[:age, '>', 2], [:age, '<', 5]]) # returns all dogs between the age of 2 and 5 (exclusive)
96
+ # find_by_kind(:dog, :sorts => [[:name, :asc]]) # returns all dogs in alphebetical order of their name
97
+ # 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
98
+ # find_by_kind(:dog, :limit => 10) # returns upto 10 dogs in undefined order
99
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10) # returns upto the first 10 dogs in alphebetical order of their name
100
+ # find_by_kind(:dog, :sorts => [[:name, :asc]], :limit => 10, :offset => 10) # returns the second set of 10 dogs in alphebetical order of their name
101
+ #
102
+ # Filter operations and acceptable syntax:
103
+ # "=" "eq"
104
+ # "<" "lt"
105
+ # "<=" "lte"
106
+ # ">" "gt"
107
+ # ">=" "gte"
108
+ # "!=" "not"
109
+ # "contains?" "contains" "in?" "in"
110
+ #
111
+ # Sort orders and acceptable syntax:
112
+ # :asc "asc" :ascending "ascending"
113
+ # :desc "desc" :descending "descending"
114
+ def find_by_kind(kind, args={})
115
+ unpack_records(datastore.find(build_query(kind, args)))
116
+ end
117
+
118
+ # Removes the record stored with the given key. Returns nil no matter what.
119
+ def delete_by_key(kind, key)
120
+ datastore.delete_by_key(kind, key)
121
+ end
122
+
123
+ # Deletes all records of the specified kind that match the filters provided.
124
+ def delete_by_kind(kind, args={})
125
+ datastore.delete(build_query(kind, args))
126
+ end
127
+
128
+ # Counts records of the specified kind that match the filters provided.
129
+ def count_by_kind(kind, args={})
130
+ datastore.count(build_query(kind, args))
131
+ end
132
+
133
+
134
+
135
+ # NOTE: these methods were marked as private in Hyperion, but were actually
136
+ # accessed from outside, in the FieldSpec class. Temporarily made public until
137
+ # things are cleand up
138
+ def packer_for(type)
139
+ @packers[type]
140
+ end
141
+
142
+ def unpacker_for(type)
143
+ @unpackers[type]
144
+ end
145
+
146
+
147
+ private
148
+
149
+ def packers
150
+ @packers ||= {}
151
+ end
152
+
153
+ def unpackers
154
+ @unpackers ||= {}
155
+ end
156
+
157
+ def build_query(kind, args)
158
+ kind = Format.format_kind(kind)
159
+ filters = build_filters(kind, args[:filters])
160
+ sorts = build_sorts(kind, args[:sorts])
161
+ Query.new(kind, filters, sorts, args[:limit], args[:offset])
162
+ end
163
+
164
+ def build_filters(kind, filters)
165
+ (filters || []).map do |(field, operator, value)|
166
+ operator = Format.format_operator(operator)
167
+ packed_field = pack_field(kind, field)
168
+ value = pack_value(kind, field, value)
169
+ Filter.new(packed_field, operator, value)
170
+ end
171
+ end
172
+
173
+ def build_sorts(kind, sorts)
174
+ (sorts || []).map do |(field, order)|
175
+ field = pack_field(kind, field)
176
+ order = Format.format_order(order)
177
+ Sort.new(field, order)
178
+ end
179
+ end
180
+
181
+ def unpack_records(records)
182
+ records.map do |record|
183
+ unpack_record(record)
184
+ end
185
+ end
186
+
187
+ def unpack_record(record)
188
+ if record
189
+ create_entity(record) do |record, entity, field, field_spec|
190
+ value = record[field_spec.db_name]
191
+ entity[field] = field_spec.unpack(value)
192
+ entity
193
+ end
194
+ end
195
+ end
196
+
197
+ def pack_records(records)
198
+ records.map do |record|
199
+ pack_record(record)
200
+ end
201
+ end
202
+
203
+ def pack_record(record)
204
+ if record
205
+ entity = create_entity(record) do |record, entity, field, field_spec|
206
+ value = record[field]
207
+ entity[field_spec.db_name] = field_spec.pack(value || field_spec.default)
208
+ entity
209
+ end
210
+ update_timestamps(entity)
211
+ end
212
+ end
213
+
214
+ def pack_field(kind, field)
215
+ field = Format.format_field(field)
216
+ kind_spec = kind_spec_for(kind)
217
+ return field unless kind_spec
218
+ field_spec = kind_spec.fields[field]
219
+ return field unless field_spec
220
+ return field_spec.db_name
221
+ end
222
+
223
+ def pack_value(kind, field, value)
224
+ kind_spec = kind_spec_for(kind)
225
+ return value unless kind_spec
226
+ field_spec = kind_spec.fields[field]
227
+ return value unless field_spec
228
+ return field_spec.pack(value)
229
+ end
230
+
231
+ def update_timestamps(record)
232
+ new?(record) ? update_created_at(record) : update_updated_at(record)
233
+ end
234
+
235
+ def update_updated_at(record)
236
+ spec = kind_spec_for(record[:kind])
237
+ if spec && spec.fields.include?(:updated_at)
238
+ record[:updated_at] = Time.now
239
+ end
240
+ record
241
+ end
242
+
243
+ def update_created_at(record)
244
+ spec = kind_spec_for(record[:kind])
245
+ if spec && spec.fields.include?(:created_at)
246
+ record[:created_at] = Time.now
247
+ end
248
+ record
249
+ end
250
+
251
+ def create_entity(record)
252
+ record = Format.format_record(record)
253
+ kind = record[:kind]
254
+ spec = kind_spec_for(kind)
255
+ unless spec
256
+ record
257
+ else
258
+ key = record[:key]
259
+ base_record = {:kind => kind}
260
+ base_record[:key] = key if key
261
+ spec.fields.reduce(base_record) do |new_record, (name, spec)|
262
+ yield(record, new_record, name, spec)
263
+ end
264
+ end
265
+ end
266
+
267
+ def kind_spec_for(kind)
268
+ @kind_specs ||= {}
269
+ @kind_specs[kind]
270
+ end
271
+
272
+ def save_kind_spec(kind_spec)
273
+ @kind_specs ||= {}
274
+ @kind_specs[kind_spec.kind] = kind_spec
275
+ end
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+ end
284
+ end
@@ -0,0 +1,422 @@
1
+ require 'endymion/types'
2
+
3
+ shared_examples_for 'Datastore' do
4
+
5
+ let(:api) do
6
+ Endymion::API.new(datastore)
7
+ end
8
+
9
+ # before(:each) do
10
+ # api.defentity(:shirt) do |kind|
11
+ # kind.field(:account_key, :type => Hyperion::Types.foreign_key(:account), :db_name => :account_id)
12
+ # end
13
+ #
14
+ # api.defentity(:account) do |kind|
15
+ # kind.field(:first_name)
16
+ # end
17
+ #
18
+ #
19
+ # end
20
+
21
+ context 'save' do
22
+
23
+ it 'saves a hash and returns it' do
24
+ record = api.save({:kind => 'testing', :name => 'ann'})
25
+ record[:kind].should == 'testing'
26
+ record[:name].should == 'ann'
27
+ end
28
+
29
+ it 'saves an empty record' do
30
+ record = api.save({:kind => 'testing'})
31
+ record[:kind].should == 'testing'
32
+ end
33
+
34
+ it 'assigns a key to new records' do
35
+ record = api.save({:kind => 'testing', :name => 'ann'})
36
+ record[:key].should_not be_nil
37
+ end
38
+
39
+ it 'saves an existing record' do
40
+ record1 = api.save({:kind => 'other_testing', :name => 'ann'})
41
+ record2 = api.save({:kind => 'other_testing', :key => record1[:key]}, :name => 'james')
42
+ record1[:key].should == record2[:key]
43
+ api.find_by_kind('other_testing').length.should == 1
44
+ end
45
+
46
+ it 'saves an existing record to be empty' do
47
+ record1 = api.save({:kind => 'other_testing', :name => 'ann'})
48
+ record2 = record1.dup
49
+ record2.delete(:name)
50
+ record2 = api.save(record2)
51
+ record1[:key].should == record2[:key]
52
+ api.find_by_kind('other_testing').length.should == 1
53
+ end
54
+
55
+ def ten_testing_records(kind = 'testing')
56
+ (1..10).to_a.map do |i|
57
+ {:kind => kind, :name => i.to_s}
58
+ end
59
+ end
60
+
61
+ it 'assigns unique keys to each record' do
62
+ keys = ten_testing_records.map do |record|
63
+ api.save(record)[:key]
64
+ end
65
+ unique_keys = Set.new(keys)
66
+ unique_keys.length.should == 10
67
+ end
68
+
69
+ it 'can save many records' do
70
+ saved_records = api.save_many(ten_testing_records)
71
+ saved_records.length.should == 10
72
+ saved_names = Set.new(saved_records.map { |record| record[:name] })
73
+ found_records = api.find_by_kind('testing')
74
+ found_records.length.should == 10
75
+ found_names = Set.new(found_records.map { |record| record[:name] })
76
+ found_names.should == saved_names
77
+ end
78
+
79
+ end
80
+
81
+ context 'create' do
82
+
83
+ it 'saves a hash without a key and returns it' do
84
+ record = api.create({:kind => 'testing', :name => 'ann'})
85
+ record[:kind].should == 'testing'
86
+ record[:name].should == 'ann'
87
+ end
88
+
89
+ it 'assigns a key to records without keys' do
90
+ record = api.create({:kind => 'testing', :name => 'ann'})
91
+ record[:key].should_not be_nil
92
+ end
93
+
94
+ it 'saves a hash with an unpacked key and returns it' do
95
+ unpacked_key = "123"
96
+ record = api.create({:kind => 'testing', :key => unpacked_key, :name => 'ann'})
97
+ record[:kind].should == 'testing'
98
+ record[:name].should == 'ann'
99
+ record[:key].to_s.should == unpacked_key
100
+ end
101
+
102
+ it 'saves an empty record' do
103
+ record = api.create({:kind => 'testing'})
104
+ record[:kind].should == 'testing'
105
+ end
106
+
107
+ it 'raises an error with an existing record' do
108
+ record = api.create({:kind => 'other_testing', :name => 'ann'})
109
+ expect do
110
+ api.create({:kind => 'other_testing', :key => record[:key]}, :name => 'james')
111
+ end.to raise_error
112
+ end
113
+
114
+ def ten_testing_records(kind = 'testing')
115
+ (1..10).to_a.map do |i|
116
+ {:kind => kind, :name => i.to_s}
117
+ end
118
+ end
119
+
120
+ it 'assigns unique keys to each record' do
121
+ keys = ten_testing_records.map do |record|
122
+ api.create(record)[:key]
123
+ end
124
+ unique_keys = Set.new(keys)
125
+ unique_keys.length.should == 10
126
+ end
127
+
128
+ it 'can create many records' do
129
+ saved_records = api.create_many(ten_testing_records)
130
+ saved_records.length.should == 10
131
+ saved_names = Set.new(saved_records.map { |record| record[:name] })
132
+ found_records = api.find_by_kind('testing')
133
+ found_records.length.should == 10
134
+ found_names = Set.new(found_records.map { |record| record[:name] })
135
+ found_names.should == saved_names
136
+ end
137
+ end
138
+
139
+ def remove_nils(record)
140
+ record.reduce({}) do |non_nil_record, (field, value)|
141
+ non_nil_record[field] = value unless value.nil?
142
+ non_nil_record
143
+ end
144
+ end
145
+
146
+ context 'find by key' do
147
+ it 'finds by key' do
148
+ record = api.save({:kind => 'testing', :inti => 5})
149
+ remove_nils(api.find_by_key('testing', record[:key])).should == remove_nils(record)
150
+ end
151
+
152
+ it 'returns nil on non-existent keys' do
153
+ expect(api.find_by_key('testing', 1)).to be_nil
154
+ end
155
+ end
156
+
157
+ context 'find by kind' do
158
+
159
+ it 'filters by the kind' do
160
+ api.save({:kind => 'other_testing', :inti => 5})
161
+ found_records = api.find_by_kind('testing')
162
+ found_records.each do |record|
163
+ record[:kind].should == 'testing'
164
+ end
165
+ end
166
+
167
+ it "can't filter on old values" do
168
+ record = api.save(:kind => 'testing', :inti => 12)
169
+ api.save(record, :inti => 2)
170
+ api.find_by_kind('testing', :filters => [[:inti, '=', 12]]).should == []
171
+ end
172
+
173
+ context 'filters' do
174
+ before :each do
175
+ api.save_many([
176
+ {:kind => 'testing', :inti => 1, :data => 'one' },
177
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
178
+ {:kind => 'testing', :inti => 23, :data => 'twenty3'},
179
+ {:kind => 'testing', :inti => 34, :data => 'thirty4'},
180
+ {:kind => 'testing', :inti => 45, :data => 'forty5' },
181
+ {:kind => 'testing', :inti => 1, :data => 'the one'},
182
+ {:kind => 'testing', :inti => 44, :data => 'forty4' },
183
+ {:kind => 'testing', :inti => nil, :data => 'forty4' }
184
+ ])
185
+ end
186
+
187
+ [
188
+ [[[:inti, '<', 25]], [1, 12, 23], :inti],
189
+ [[[:inti, '<=', 25]], [1, 12, 23], :inti],
190
+ [[[:inti, '>', 25]], [34, 44, 45], :inti],
191
+ [[[:inti, '=', 34]], [34], :inti],
192
+ [[[:inti, '=', nil]], [nil], :inti],
193
+ [[[:inti, '!=', nil]], [1, 1, 12, 23, 34, 44, 45], :inti],
194
+ [[[:inti, '!=', 34]], [1, 12, 23, 44, 45, nil], :inti],
195
+ [[[:inti, 'in', [12, 34]]], [12, 34], :inti],
196
+ [[[:inti, '>', 10], [:inti, '<', 25]], [12, 23], :inti],
197
+ [[[:inti, '<', 25], [:inti, '>', 10]], [12, 23], :inti],
198
+ [[[:inti, '>', 25], [:data, '<', 'thirty4']], [44, 45], :inti],
199
+ [[[:data, '<', 'thirty4'], [:inti, '>', 25]], [44, 45], :inti],
200
+ [[[:inti, '>', 10], [:inti, '<', 25], [:inti, '=', 23]], [23], :inti],
201
+ [[[:inti, '=', 23], [:inti, '>', 10], [:inti, '<', 25]], [23], :inti],
202
+ [[[:inti, '<', 24], [:inti, '>', 25]], [], :inti],
203
+ [[[:inti, '!=', 12], [:inti, '!=', 23], [:inti, '!=', 34]], [1, 44, 45, nil], :inti],
204
+ [[[:data, '<', 'qux']], ['one', 'forty4', 'forty5'], :data],
205
+ [[[:data, '<=', 'one']], ['one', 'forty4', 'forty5'], :data],
206
+ [[[:data, '>=', 'thirty4']], ['twelve', 'twenty3', 'thirty4'], :data],
207
+ [[[:data, '=', 'one']], ['one'], :data],
208
+ [[[:data, '!=', 'one']], ['the one', 'twelve', 'twenty3', 'thirty4', 'forty4', 'forty5'], :data],
209
+ [[[:data, 'in', ['one', 'twelve']]], ['one', 'twelve'], :data],
210
+ [[[:data, '>', 'qux'], [:data, '<', 'qux']], [], :data],
211
+ [[[:data, '!=', 'one'], [:data, '!=', 'twelve'], [:data, '!=', 'twenty3']], ['the one', 'thirty4', 'forty4', 'forty5'], :data],
212
+ ].each do |filters, result, field|
213
+
214
+ it filters.map(&:to_s).join(', ') do
215
+ found_records = api.find_by_kind('testing', :filters => filters)
216
+ ints = Set.new(found_records.map {|record| record[field]})
217
+ ints.should == Set.new(result)
218
+ end
219
+ end
220
+
221
+ end
222
+
223
+ context 'sorts' do
224
+ before :each do
225
+ api.save_many([
226
+ {:kind => 'testing', :inti => 1, :data => 'one' },
227
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
228
+ {:kind => 'testing', :inti => 23, :data => 'twenty3'},
229
+ {:kind => 'testing', :inti => 34, :data => 'thirty4'},
230
+ {:kind => 'testing', :inti => 45, :data => 'forty5' },
231
+ {:kind => 'testing', :inti => 1, :data => 'the one'},
232
+ {:kind => 'testing', :inti => 44, :data => 'forty4' },
233
+ ])
234
+ end
235
+
236
+ [
237
+ [[[:inti, :asc]], [1, 1, 12, 23, 34, 44, 45], :inti],
238
+ [[[:inti, :desc]], [45, 44, 34, 23, 12, 1, 1], :inti],
239
+ [[[:data, :asc]], [44, 45, 1, 1, 34, 12, 23], :inti],
240
+ [[[:data, :desc]], [23, 12, 34, 1, 1, 45, 44], :inti],
241
+ [[[:inti, :asc], [:data, :asc]], ['one', 'the one', 'twelve', 'twenty3', 'thirty4', 'forty4', 'forty5'], :data],
242
+ [[[:data, :asc], [:inti, :asc]], [44, 45, 1, 1, 34, 12, 23], :inti]
243
+ ].each do |sorts, result, field|
244
+
245
+ it sorts.map(&:to_s).join(', ') do
246
+ found_records = api.find_by_kind('testing', :sorts => sorts)
247
+ ints = found_records.map {|record| record[field]}
248
+ ints.should == result
249
+ end
250
+ end
251
+ end
252
+
253
+ context 'limit and offset' do
254
+ before :each do
255
+ api.save_many([
256
+ {:kind => 'testing', :inti => 1, :data => 'one' },
257
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
258
+ {:kind => 'testing', :inti => 23, :data => 'twenty3'},
259
+ {:kind => 'testing', :inti => 34, :data => 'thirty4'},
260
+ {:kind => 'testing', :inti => 45, :data => 'forty5' },
261
+ {:kind => 'testing', :inti => 1, :data => 'the one'},
262
+ {:kind => 'testing', :inti => 44, :data => 'forty4' },
263
+ ])
264
+ end
265
+
266
+ specify 'offset n returns results starting at the nth record' do
267
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :offset => 2)
268
+ ints = found_records.map {|record| record[:inti]}
269
+ ints.should == [12, 23, 34, 44, 45]
270
+ end
271
+
272
+ specify 'limit n takes only the first n records' do
273
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :limit => 2)
274
+ found_records.map {|record| record[:inti]}.should == [1, 1]
275
+
276
+ found_records = api.find_by_kind('testing', :sorts => [[:inti, :asc]], :limit => 1_000_000)
277
+ found_records.map {|record| record[:inti]}.should == [1, 1, 12, 23, 34, 44, 45]
278
+ end
279
+
280
+ [
281
+ [{:limit => 2, :offset => 2}, [[:inti, :asc]], [12, 23]],
282
+ [{:limit => 2, :offset => 4}, [[:inti, :asc]], [34, 44]],
283
+ [{:limit => 2} , [[:inti, :desc]], [45, 44]],
284
+ [{:limit => 2, :offset => 2}, [[:inti, :desc]], [34, 23]],
285
+ [{:limit => 2, :offset => 4}, [[:inti, :desc]], [12, 1]],
286
+ ].each do |constraints, sorts, result|
287
+ example constraints.inspect do
288
+ found_records = api.find_by_kind 'testing', constraints.merge(:sorts => sorts)
289
+ found_records.map { |record| record[:inti] }.should == result
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ context 'delete' do
296
+
297
+ before :each do
298
+ api.save_many([
299
+ {:kind => 'testing', :inti => 1, :data => 'one' },
300
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
301
+ {:kind => 'testing', :inti => nil, :data => 'twelve' }
302
+ ])
303
+ end
304
+
305
+ it 'deletes by key' do
306
+ records = api.find_by_kind('testing')
307
+ record_to_delete = records.first
308
+ api.delete_by_key(record_to_delete[:kind], record_to_delete[:key]).should be_nil
309
+ api.find_by_kind('testing').should_not include(record_to_delete)
310
+ end
311
+
312
+ it 'returns nil when deleting non-existent keys' do
313
+ expect(api.delete_by_key('testing', 1)).to be_nil
314
+ end
315
+
316
+ context 'filters' do
317
+
318
+ [
319
+ [[], []],
320
+ [[[:inti, '=', 1]], [12, nil]],
321
+ [[[:data, '=', 'one']], [12, nil]],
322
+ [[[:inti, '!=', 1]], [1]],
323
+ [[[:inti, '<=', 1]], [12, nil]],
324
+ [[[:inti, '<=', 2]], [12, nil]],
325
+ [[[:inti, '>=', 2]], [1, nil]],
326
+ [[[:inti, '>', 1]], [1, nil]],
327
+ [[[:inti, 'in', [1]]], [12, nil]],
328
+ [[[:inti, 'in', [1, nil]]], [12]],
329
+ [[[:inti, 'in', [1, 12]]], [nil]],
330
+ [[[:inti, '=', 2]], [1, 12, nil]],
331
+ [[[:inti, '=', nil]], [1, 12]],
332
+ [[[:inti, '!=', nil]], [nil]],
333
+ ].each do |filters, result|
334
+ it filters.inspect do
335
+ api.delete_by_kind('testing', :filters => filters)
336
+ intis = api.find_by_kind('testing').map {|r| r[:inti]}
337
+ intis.should =~ result
338
+ end
339
+ end
340
+
341
+ end
342
+ end
343
+
344
+ context 'count' do
345
+
346
+ before :each do
347
+ api.save_many([
348
+ {:kind => 'testing', :inti => 1, :data => 'one' },
349
+ {:kind => 'testing', :inti => 12, :data => 'twelve' },
350
+ {:kind => 'testing', :inti => nil, :data => 'twelve' }
351
+ ])
352
+ end
353
+
354
+ context 'filters' do
355
+
356
+ [
357
+ [[], 3],
358
+ [[[:inti, '=', 1]], 1],
359
+ [[[:data, '=', 'one']], 1],
360
+ [[[:inti, '!=', 1]], 2],
361
+ [[[:inti, '<=', 1]], 1],
362
+ [[[:inti, '<=', 2]], 1],
363
+ [[[:inti, '>=', 2]], 1],
364
+ [[[:inti, '>', 1]], 1],
365
+ [[[:inti, 'in', [1]]], 1],
366
+ [[[:inti, 'in', [1, 12]]], 2],
367
+ [[[:inti, '=', 2]], 0],
368
+ [[[:inti, '=', nil]], 1],
369
+ [[[:inti, '!=', nil]], 2],
370
+ ].each do |filters, result|
371
+ it filters.inspect do
372
+ api.count_by_kind('testing', :filters => filters).should == result
373
+ end
374
+ end
375
+
376
+ end
377
+ end
378
+
379
+ # context 'foreign_keys' do
380
+ # it 'saves records with foreign keys' do
381
+ # account = api.save(:kind => :account)
382
+ # account_key = account[:key]
383
+ # shirt = api.save(:kind => :shirt, :account_key => account_key)
384
+ # found_shirt = api.find_by_key(:shirt, shirt[:key])
385
+ # found_account = api.find_by_key(:account, account_key)
386
+ # shirt[:account_key].should == account_key
387
+ # found_shirt[:account_key].should == account_key
388
+ # found_account[:key].should == account_key
389
+ # end
390
+ #
391
+ # it 'filters on foreign keys' do
392
+ # account = api.save(:kind => :account)
393
+ # account_key = account[:key]
394
+ # shirt = api.save(:kind => :shirt, :account_key => account_key)
395
+ # found_shirts = api.find_by_kind(:shirt, :filters => [[:account_key, '=', account_key]])
396
+ # found_shirts[0].should == shirt
397
+ # end
398
+ #
399
+ # it 'filters on multiple foreign keys' do
400
+ # account_keys = (1..2).map { api.save(:kind => :account)[:key] }
401
+ # shirt1 = api.save(:kind => :shirt, :account_key => account_keys[0])
402
+ # shirt2 = api.save(:kind => :shirt, :account_key => account_keys[1])
403
+ # found_shirts = api.find_by_kind(:shirt, :filters => [[:account_key, 'in', account_keys]])
404
+ # found_shirts.should include(shirt1)
405
+ # found_shirts.should include(shirt2)
406
+ # end
407
+ #
408
+ # it 'unpacks nil foreign keys' do
409
+ # shirt = api.save(:kind => :shirt, :account_key => nil)
410
+ # shirt[:account_key].should be_nil
411
+ # found_shirt = api.find_by_kind(:shirt, :filters => [[:account_key, '=', nil]]).first
412
+ # found_shirt[:account_key].should be_nil
413
+ # end
414
+ #
415
+ # it 'filters on nil foreign key' do
416
+ # account = api.save(:kind => :account)
417
+ # shirt = api.save(:kind => :shirt, :account_key => account[:key])
418
+ # nil_shirt = api.save(:kind => :shirt, :account_key => nil)
419
+ # api.find_by_kind(:shirt, :filters => [[:account_key, '=', nil]]).should == [nil_shirt]
420
+ # end
421
+ # end
422
+ end
@@ -0,0 +1,53 @@
1
+ module Endymion
2
+ class FakeDs
3
+
4
+ attr_accessor :saved_records, :created_records, :queries, :key_queries,
5
+ :returns, :key_pack_queries,:key_unpack_queries
6
+
7
+ def initialize(opts={})
8
+ @saved_records = []
9
+ @created_records = []
10
+ @returns = []
11
+ @queries = []
12
+ @key_queries = []
13
+ @key_pack_queries = []
14
+ @key_unpack_queries = []
15
+ end
16
+
17
+ def save(records)
18
+ @saved_records += records
19
+ returns.shift || []
20
+ end
21
+
22
+ def create(records)
23
+ @created_records += records
24
+ returns.shift || []
25
+ end
26
+
27
+ def find_by_key(kind, key)
28
+ @key_queries << [kind, key]
29
+ returns.shift || nil
30
+ end
31
+
32
+ def find(query)
33
+ @queries << query
34
+ returns.shift || []
35
+ end
36
+
37
+ def delete_by_key(kind, key)
38
+ @key_queries << [kind, key]
39
+ nil
40
+ end
41
+
42
+ def delete(query)
43
+ @queries << query
44
+ returns.shift || nil
45
+ end
46
+
47
+ def count(query)
48
+ @queries << query
49
+ returns.shift || 0
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ require 'endymion/format'
2
+
3
+ module Endymion
4
+ class FieldSpec
5
+
6
+ attr_reader :name, :default, :db_name
7
+
8
+ def initialize(name, opts={})
9
+ @name = name
10
+ @default = opts[:default]
11
+ @type = opts[:type]
12
+ @packer = opts[:packer]
13
+ @unpacker = opts[:unpacker]
14
+ @db_name = opts[:db_name] ? Format.format_field(opts[:db_name]) : name
15
+ @api = opts[:api]
16
+ end
17
+
18
+ def pack(value)
19
+ if @packer && @packer.respond_to?(:call)
20
+ @packer.call(value)
21
+ elsif @type
22
+ type_packer = @api.packer_for(@type)
23
+ type_packer ? type_packer.call(value) : value
24
+ else
25
+ value
26
+ end
27
+ end
28
+
29
+ def unpack(value)
30
+ if @unpacker && @unpacker.respond_to?(:call)
31
+ @unpacker.call(value)
32
+ elsif @type
33
+ type_packer = @api.unpacker_for(@type)
34
+ type_packer ? type_packer.call(value) : value
35
+ else
36
+ value
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ module Endymion
2
+ class Filter
3
+
4
+ attr_reader :operator, :field, :value
5
+
6
+ def initialize(field, operator, value)
7
+ @operator = operator
8
+ @field = field
9
+ @value = value
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ :operator => operator,
15
+ :field => field,
16
+ :value => value
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ require 'endymion/util'
2
+
3
+ module Endymion
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
+ field.to_sym
14
+ end
15
+
16
+ def format_record(record)
17
+ record = record.reduce({}) do |new_record, (field_name, value)|
18
+ new_record[format_field(field_name)] = 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
+
@@ -0,0 +1,14 @@
1
+ require 'base64'
2
+ require 'uuidtools'
3
+
4
+ module Endymion
5
+ class Key
6
+ class << self
7
+
8
+ def generate_id
9
+ UUIDTools::UUID.random_create.to_s.gsub(/-/, '')
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ require 'endymion/format'
2
+
3
+ module Endymion
4
+ class KindSpec
5
+
6
+ attr_reader :kind, :fields, :api
7
+
8
+ def initialize(kind, api)
9
+ @kind = kind
10
+ @fields = {}
11
+ @api = api
12
+ end
13
+
14
+ def field(name, opts={})
15
+ name = Format.format_field(name)
16
+ @fields[name] = FieldSpec.new(name, opts.merge(api: api))
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,75 @@
1
+ require 'endymion'
2
+ require 'endymion/key'
3
+ require 'endymion/memory/helper'
4
+
5
+ module Endymion
6
+ class Memory
7
+
8
+ def initialize(opts={})
9
+ @store = {}
10
+ end
11
+
12
+ def save(records)
13
+ records.map do |record|
14
+ key = Endymion.new?(record) ? generate_key : record[:key]
15
+ record[:key] = key
16
+ store[key] = record
17
+ record
18
+ end
19
+ end
20
+
21
+ def create(records)
22
+ records.map do |record|
23
+ raise 'duplicate key' if record[:key] && store.key?(record[:key])
24
+ record[:key] ||= generate_key
25
+ store[record[:key]] = record
26
+ record
27
+ end
28
+ end
29
+
30
+ def find_by_key(kind, key)
31
+ store[key]
32
+ end
33
+
34
+ def find(query)
35
+ records = store.values
36
+ records = filter_kind(query.kind, records)
37
+ records = Helper.apply_filters(query.filters, records)
38
+ records = Helper.apply_sorts(query.sorts, records)
39
+ records = Helper.apply_offset(query.offset, records)
40
+ records = Helper.apply_limit(query.limit, records)
41
+ records
42
+ end
43
+
44
+ def delete_by_key(kind, key)
45
+ store.delete(key)
46
+ nil
47
+ end
48
+
49
+ def delete(query)
50
+ records = find(query)
51
+ store.delete_if do |key, record|
52
+ records.include?(record)
53
+ end
54
+ nil
55
+ end
56
+
57
+ def count(query)
58
+ find(query).length
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :store
64
+
65
+ def filter_kind(kind, records)
66
+ records.select do |record|
67
+ record[:kind] == kind
68
+ end
69
+ end
70
+
71
+ def generate_key
72
+ Endymion::Key.generate_id
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,52 @@
1
+ module Endymion
2
+ class Memory
3
+ module Helper
4
+ def self.apply_filters(filters, records)
5
+ records.select do |record|
6
+ filters.all? do |filter|
7
+ value = record[filter.field]
8
+ case filter.operator
9
+ when '<'; value && value < filter.value
10
+ when '<='; value && value <= filter.value
11
+ when '>'; value && value > filter.value
12
+ when '>='; value && value >= filter.value
13
+ when '='; value == filter.value
14
+ when '!='; value != filter.value
15
+ when 'contains?'; filter.value.include?(value)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.apply_sorts(sorts, records)
22
+ records.sort { |record1, record2| compare_records record1, record2, sorts }
23
+ end
24
+
25
+ def self.compare_records(record1, record2, sorts)
26
+ sorts.each do |sort|
27
+ result = compare_record record1, record2, sort
28
+ return result if result
29
+ end
30
+ 0
31
+ end
32
+
33
+ def self.compare_record(record1, record2, sort)
34
+ field1, field2 = record1[sort.field], record2[sort.field]
35
+ field1 == field2 ? nil :
36
+ field1 < field2 && sort.ascending? ? -1 :
37
+ field1 > field2 && sort.descending? ? -1 : 1
38
+ end
39
+
40
+ def self.apply_offset(offset, records)
41
+ return records unless offset
42
+ records.drop offset
43
+ end
44
+
45
+ def self.apply_limit(limit, records)
46
+ return records unless limit
47
+ records.take(limit)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,24 @@
1
+ module Endymion
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
+ 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
+
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module Endymion
2
+ class Sort
3
+
4
+ attr_reader :field, :order
5
+
6
+ def initialize(field, order)
7
+ @field = field
8
+ @order = order
9
+ end
10
+
11
+ def ascending?
12
+ order == :asc
13
+ end
14
+
15
+ def descending?
16
+ order == :desc
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ :field => field,
22
+ :order => order
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require 'endymion'
2
+ require 'endymion/format'
3
+
4
+ module Endymion
5
+ class Types
6
+ class << self
7
+
8
+ # def foreign_key(kind)
9
+ # kind_key = "#{Format.format_kind(kind)}_key".to_sym
10
+ # unless Hyperion.packer_defined?(kind_key)
11
+ # Hyperion.pack(kind_key) do |key|
12
+ # key
13
+ # end
14
+ # Hyperion.unpack(kind_key) do |key|
15
+ # key
16
+ # end
17
+ # end
18
+ # kind_key
19
+ # end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ module Endymion
2
+ class Util
3
+
4
+ class << self
5
+
6
+ def camel_case(str)
7
+ cameled = str.gsub(/[_| |\-][A-Za-z]/) { |a| a[1..-1].upcase } if str
8
+ uncapitalize(cameled)
9
+ end
10
+
11
+ def class_name(str)
12
+ capitalize(camel_case(str))
13
+ end
14
+
15
+ def snake_case(str)
16
+ str.gsub(/([a-z0-9])([A-Z])/, '\1 \2').downcase.gsub(/[ |\-]/, '_') if str
17
+ end
18
+
19
+ def capitalize(str)
20
+ do_to_first(str) do |first_letter|
21
+ first_letter.upcase
22
+ end
23
+ end
24
+
25
+ def uncapitalize(str)
26
+ do_to_first(str) do |first_letter|
27
+ first_letter.downcase
28
+ end
29
+ end
30
+
31
+ def do_to_first(str)
32
+ if str
33
+ first = yield(str[0, 1])
34
+ if str.length > 1
35
+ last = str[1..-1]
36
+ first + last
37
+ else
38
+ first
39
+ end
40
+ end
41
+ end
42
+
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
52
+
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: endymion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David Faber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Key/Value persistence interface. Forked from Hyperion.
14
+ email: dafaber@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/endymion.rb
20
+ - lib/endymion/api.rb
21
+ - lib/endymion/dev/ds_spec.rb
22
+ - lib/endymion/fake_ds.rb
23
+ - lib/endymion/field_spec.rb
24
+ - lib/endymion/filter.rb
25
+ - lib/endymion/format.rb
26
+ - lib/endymion/key.rb
27
+ - lib/endymion/kind_spec.rb
28
+ - lib/endymion/memory.rb
29
+ - lib/endymion/memory/helper.rb
30
+ - lib/endymion/query.rb
31
+ - lib/endymion/sort.rb
32
+ - lib/endymion/types.rb
33
+ - lib/endymion/util.rb
34
+ homepage:
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.2.2
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Key/Value persistence interface. Forked from Hyperion.
58
+ test_files: []