endymion 0.0.1

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.
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: []