orchestrate 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +40 -4
- data/lib/orchestrate/application.rb +13 -3
- data/lib/orchestrate/client.rb +10 -1
- data/lib/orchestrate/collection.rb +132 -9
- data/lib/orchestrate/graph.rb +146 -0
- data/lib/orchestrate/key_value.rb +60 -19
- data/lib/orchestrate/version.rb +1 -1
- data/lib/orchestrate.rb +1 -0
- data/test/orchestrate/application_test.rb +10 -0
- data/test/orchestrate/client_test.rb +17 -0
- data/test/orchestrate/collection_enumeration_test.rb +79 -36
- data/test/orchestrate/collection_kv_accessors_test.rb +16 -0
- data/test/orchestrate/collection_searching_test.rb +101 -0
- data/test/orchestrate/key_value_persistence_test.rb +63 -1
- data/test/orchestrate/key_value_test.rb +2 -12
- data/test/orchestrate/relations_test.rb +128 -0
- data/test/test_helper.rb +7 -3
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9ab618bc8f0fd8e7d46d7621b4378a894ec9150
|
4
|
+
data.tar.gz: 4869b43cead19ce0c710d135f6551b20e0ef991a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b22bac7d118ed9f23c25f6f3f0dd8e624d99d3f0eb7da3c85b25fedb59fa280494087945eee367d08cb730ef78d8f39a5beac2c3bcddd179d808e3e07f77bc05
|
7
|
+
data.tar.gz: f72465848c71bbcbde91f021ce3f6097d006d5da3bd4177a3867534b1e201d01ea3d042ed3b158049852fa38107694af611397519c774572afd30f947be2d486
|
data/README.md
CHANGED
@@ -4,7 +4,9 @@ Orchestrate API for Ruby
|
|
4
4
|
|
5
5
|
Ruby client interface for the [Orchestrate.io](http://orchestrate.io) REST API.
|
6
6
|
|
7
|
-
[rDoc Documentation]
|
7
|
+
[rDoc Documentation][rdoc]
|
8
|
+
|
9
|
+
[rdoc]: (http://rdoc.info/github/orchestrate-io/orchestrate-ruby/master/frames)
|
8
10
|
|
9
11
|
## Getting Started
|
10
12
|
|
@@ -53,6 +55,12 @@ client.delete(:users, :jack, jack.ref) # DELETE-If-Match, returns API::Resp
|
|
53
55
|
client.list(:users) # LIST users, returns API::CollectionResposne
|
54
56
|
```
|
55
57
|
|
58
|
+
### Examples and Documentation
|
59
|
+
|
60
|
+
There are more examples at [Orchestrate's API Documentation][apidoc] and documentation in the [rdoc][].
|
61
|
+
|
62
|
+
[apidoc]: http://orchestrate.io/api/version
|
63
|
+
|
56
64
|
## Swapping out the HTTP back end
|
57
65
|
|
58
66
|
This gem uses [Faraday][] for its HTTP needs -- and Faraday allows you to change the underlying HTTP client used. The Orchestrate client defaults to [net-http-persistent][nhp] for speed on repeat requests without having to resort to a compiled library. You can easily swap in [Typhoeus][] which uses libcurl to enable fast, parallel requests, or [EventMachine HTTP][em-http] to use a non-blocking, callback-based interface. Examples are below.
|
@@ -68,6 +76,8 @@ You may use Faraday's `test` adapter to stub out calls to the Orchestrate API in
|
|
68
76
|
|
69
77
|
If you're using a Faraday back end that enables parallelization, such as Typhoeus, EM-HTTP-Request, or EM-Synchrony you can use `Orchestrate::Client#in_parallel` to fire off multiple requests at once. If your Faraday back end does not support this, the method will still work as expected, but Faraday will output a warning to STDERR and the requests will be performed in series.
|
70
78
|
|
79
|
+
Note that these parallel modes are not thread-safe. If you are using the client in a threaded environment, you should use `#dup` on your `Orchestrate::Client` or `Orchestrate::Application` to create per-thread instances.
|
80
|
+
|
71
81
|
#### method client
|
72
82
|
``` ruby
|
73
83
|
client = Orchestrate::Client.new(api_key) {|f| f.adapter :typhoeus }
|
@@ -87,14 +97,27 @@ responses[:user] = #<Orchestrate::API::ItemResponse:0x00...>
|
|
87
97
|
app = Orchestrate::Application.new(api_key) {|f| f.adapter :typhoeus }
|
88
98
|
|
89
99
|
app.in_parallel do
|
90
|
-
@items = app[:my_collection].
|
100
|
+
@items = app[:my_collection].each
|
91
101
|
@user = app[:users][current_user_id]
|
92
102
|
end
|
103
|
+
@items.take(5)
|
93
104
|
```
|
94
105
|
|
95
|
-
Note that values are not available inside of the `in_parallel` block. The `r[:list]` or `@items` objects are placeholders for their future values and will be available after the `in_parallel` block returns. Since `take` and other enumerable methods normally attempt to access the value when called, you **must** convert the `app[:my_collection]`
|
106
|
+
Note that values are not available inside of the `in_parallel` block. The `r[:list]` or `@items` objects are placeholders for their future values and will be available after the `in_parallel` block returns. Since `take` and other enumerable methods normally attempt to access the value when called, you **must** convert the `app[:my_collection]` to an `Enumerator` with `#each` and access them outside the parallel block.
|
96
107
|
|
97
|
-
|
108
|
+
You can, inside the parallel block, construct further iteration over your collection with `Enumerable#lazy` like so:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
app.in_parallel do
|
112
|
+
@items = app[:my_collection].each.lazy.take(5)
|
113
|
+
...
|
114
|
+
end
|
115
|
+
@items.force
|
116
|
+
```
|
117
|
+
|
118
|
+
Attempting to access the values inside the parallel block will raise an `Orchestrate::ResultsNotReady` exception.
|
119
|
+
|
120
|
+
Lazy enumerators are not available by default in Ruby 1.9. Lazy enumerator results are not pre-fetched from orchestrate unless they are taken inside an `#in_parallel` block, otherwise results are fetched when needed.
|
98
121
|
|
99
122
|
### Using with Typhoeus
|
100
123
|
|
@@ -136,6 +159,19 @@ end
|
|
136
159
|
|
137
160
|
## Release Notes
|
138
161
|
|
162
|
+
### July 24, 2014: release 0.8.0
|
163
|
+
- **BACKWARDS-INCOMPATIBLE** Fix #69, `Client` will url-escape path segments. If you have keys with slashes or spaces or other
|
164
|
+
characters escaped by `URI.escape` the client will now behave as expected, however if you've used these keys with this client
|
165
|
+
before you may not be able to get to those old keys.
|
166
|
+
- Fix #78, KeyValues are given an empty hash value by default, instead of nil.
|
167
|
+
- Change default value for `KeyValue#ref` to be false. On save, this will send an `If-None-Match` header instead of omitting the condition.
|
168
|
+
- Revisited `#in_parallel` methods, improved documentation, tests for Enumerables on Object client, made sure behavior conforms.
|
169
|
+
- Implement `KeyValue#update` and `#update!` to update the value and save in one go.
|
170
|
+
- Implement `Collection#stub` to instantiate a KeyValue without loading it, for access to Relations, Refs, Events, etc.
|
171
|
+
- Implement `Collection#build` to provide a factory for unsaved KV items in a collection.
|
172
|
+
- Implement `KeyValue#relation` for Graph / Relation queries on object client.
|
173
|
+
- Implement `Collection#search` for Lucene queries on Collections via the object client.
|
174
|
+
|
139
175
|
### July 1, 2014: release 0.7.0
|
140
176
|
- Fix #66 to make parallel mode work properly
|
141
177
|
- Switch the default Faraday adapter to the `net-http-persistent` gem, which in casual testing yields much better performance for sustained use.
|
@@ -10,8 +10,7 @@ module Orchestrate
|
|
10
10
|
attr_reader :client
|
11
11
|
|
12
12
|
# Instantiate a new Application
|
13
|
-
# @param client_or_api_key [#to_s]
|
14
|
-
# @param client_or_api_key [Orchestrate::Client] A client instantiated with an Application and Faraday setup.
|
13
|
+
# @param client_or_api_key [Orchestrate::Client, #to_s] A client instantiated with the API key and faraday setup, or the API key for your Orchestrate Application.
|
15
14
|
# @yieldparam [Faraday::Connection] connection Setup for the Faraday connection.
|
16
15
|
# @return Orchestrate::Application
|
17
16
|
def initialize(client_or_api_key, &client_setup)
|
@@ -41,8 +40,19 @@ module Orchestrate
|
|
41
40
|
# r[:user_feed] = app.client.list_events(:users, current_user_key, :notices)
|
42
41
|
# end
|
43
42
|
# @see README See the Readme for more examples.
|
43
|
+
# @note This method is not Thread-safe. Requests generated from the same
|
44
|
+
# application instance in different threads while #in_parallel is running
|
45
|
+
# will behave unpredictably. Use `#dup` to create per-thread application
|
46
|
+
# instances.
|
44
47
|
def in_parallel(&block)
|
45
|
-
|
48
|
+
@inside_parallel = true
|
49
|
+
results = client.in_parallel(&block)
|
50
|
+
@inside_parallel = nil
|
51
|
+
results
|
52
|
+
end
|
53
|
+
|
54
|
+
def inside_parallel?
|
55
|
+
!! @inside_parallel
|
46
56
|
end
|
47
57
|
|
48
58
|
# @return a pretty-printed representation of the application.
|
data/lib/orchestrate/client.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'faraday'
|
2
|
+
require 'uri'
|
2
3
|
|
3
4
|
module Orchestrate
|
4
5
|
|
@@ -39,6 +40,11 @@ module Orchestrate
|
|
39
40
|
end
|
40
41
|
alias :inspect :to_s
|
41
42
|
|
43
|
+
# @!visibility private
|
44
|
+
def dup
|
45
|
+
self.class.new(api_key, &faraday_configuration)
|
46
|
+
end
|
47
|
+
|
42
48
|
# Tests authentication with Orchestrate.
|
43
49
|
# @return Orchestrate::API::Response
|
44
50
|
# @raise Orchestrate::API::Unauthorized if the client could not authenticate.
|
@@ -358,6 +364,9 @@ module Orchestrate
|
|
358
364
|
# r[:user_feed] = client.list_events(:users, current_user_key, :notices)
|
359
365
|
# end
|
360
366
|
# @see README See the Readme for more examples.
|
367
|
+
# @note This method is not Thread-safe. Requests generated from the same
|
368
|
+
# client in different threads while #in_parallel is running will behave
|
369
|
+
# unpredictably. Use `#dup` to create per-thread clients.
|
361
370
|
def in_parallel(&block)
|
362
371
|
accumulator = {}
|
363
372
|
http.in_parallel do
|
@@ -377,7 +386,7 @@ module Orchestrate
|
|
377
386
|
# @return API::Response
|
378
387
|
# @raise [Orchestrate::API::RequestError, Orchestrate::API::ServiceError] see http://orchestrate.io/docs/api/#errors
|
379
388
|
def send_request(method, path, options={})
|
380
|
-
path = ['/v0'].concat(path).join('/')
|
389
|
+
path = ['/v0'].concat(path.map{|s| URI.escape(s.to_s).gsub('/','%2F') }).join('/')
|
381
390
|
query_string = options.fetch(:query, {})
|
382
391
|
body = options.fetch(:body, '')
|
383
392
|
headers = options.fetch(:headers, {})
|
@@ -134,6 +134,26 @@ module Orchestrate
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
137
|
+
# Builds a new, unsaved KeyValue with the given key_name and value.
|
138
|
+
# @param key_name [#to_s] The key of the item
|
139
|
+
# @param value [#to_json] The value to store at the key.
|
140
|
+
# @return [KeyValue]
|
141
|
+
def build(key_name, value={})
|
142
|
+
kv = KeyValue.new(self, key_name)
|
143
|
+
kv.value = value
|
144
|
+
kv
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns an unloaded KeyValue object with the given key_name, if you need
|
148
|
+
# to access Refs, Relations or Events without loading the KeyValue's value.
|
149
|
+
# @param key_name [#to_s] The key of the item.
|
150
|
+
# @return [KeyValue]
|
151
|
+
def stub(key_name)
|
152
|
+
kv = KeyValue.new(self, key_name)
|
153
|
+
kv.value = nil
|
154
|
+
kv
|
155
|
+
end
|
156
|
+
|
137
157
|
# [Deletes the value for a KeyValue
|
138
158
|
# item](http://orchestrate.io/docs/api/#key/value/delete12).
|
139
159
|
# @param key_name [#to_s] The name of the key
|
@@ -166,10 +186,15 @@ module Orchestrate
|
|
166
186
|
# keys = collection.take(20).map(&:key)
|
167
187
|
# # returns the first 20 keys in the collection.
|
168
188
|
def each(&block)
|
169
|
-
return enum_for(:each) unless block
|
170
189
|
KeyValueList.new(self).each(&block)
|
171
190
|
end
|
172
191
|
|
192
|
+
# Creates a Lazy Enumerator for the Collection's KeyValue List. If called inside the app's
|
193
|
+
# `#in_parallel` block, pre-fetches results.
|
194
|
+
def lazy
|
195
|
+
KeyValueList.new(self).lazy
|
196
|
+
end
|
197
|
+
|
173
198
|
# Sets the inclusive start key for enumeration over the KeyValue items in the collection.
|
174
199
|
# @see KeyValueList#start
|
175
200
|
# @return [KeyValueList]
|
@@ -265,14 +290,13 @@ module Orchestrate
|
|
265
290
|
|
266
291
|
include Enumerable
|
267
292
|
|
268
|
-
#
|
293
|
+
# Iterates over each KeyValue item in the collection. Used as the basis for Enumerable methods.
|
269
294
|
# Items are provided in lexicographically sorted order by key name.
|
270
295
|
# @yieldparam [Orchestrate::KeyValue] key_value The KeyValue item
|
271
296
|
# @example
|
272
297
|
# keys = collection.after(:foo).take(20).map(&:key)
|
273
298
|
# # returns the first 20 keys in the collection that occur after "foo"
|
274
299
|
def each
|
275
|
-
return enum_for(:each) unless block_given?
|
276
300
|
params = {}
|
277
301
|
if range[:begin]
|
278
302
|
begin_key = range[:begin_inclusive] ? :start : :after
|
@@ -283,15 +307,24 @@ module Orchestrate
|
|
283
307
|
params[end_key] = range[:end]
|
284
308
|
end
|
285
309
|
params[:limit] = range[:limit]
|
286
|
-
response
|
287
|
-
|
310
|
+
@response ||= collection.app.client.list(collection.name, params)
|
311
|
+
return enum_for(:each) unless block_given?
|
312
|
+
raise ResultsNotReady.new if collection.app.inside_parallel?
|
288
313
|
loop do
|
289
|
-
response.results.each do |doc|
|
290
|
-
yield KeyValue.
|
314
|
+
@response.results.each do |doc|
|
315
|
+
yield KeyValue.from_listing(collection, doc, @response)
|
291
316
|
end
|
292
|
-
break unless response.next_link
|
293
|
-
response = response.next_results
|
317
|
+
break unless @response.next_link
|
318
|
+
@response = @response.next_results
|
294
319
|
end
|
320
|
+
@response = nil
|
321
|
+
end
|
322
|
+
|
323
|
+
# Creates a Lazy Enumerator for the KeyValue list. If called inside the
|
324
|
+
# app's `#in_parallel` block, will prefetch results.
|
325
|
+
def lazy
|
326
|
+
return each.lazy if collection.app.inside_parallel?
|
327
|
+
super
|
295
328
|
end
|
296
329
|
|
297
330
|
# Returns the first n items. Equivalent to Enumerable#take. Sets the
|
@@ -303,7 +336,97 @@ module Orchestrate
|
|
303
336
|
range[:limit] = count > 100 ? 100 : count
|
304
337
|
super(count)
|
305
338
|
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# @!group Searching
|
342
|
+
# [Search the items in a collection](http://orchestrate.io/docs/api/#search) using a Lucene
|
343
|
+
# Query Syntax.
|
344
|
+
# @param query [#to_s] The [Lucene Query
|
345
|
+
# String](http://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Overview)
|
346
|
+
# to query the collection with.
|
347
|
+
# @return [SearchResults] A loaded SearchResults object ready to enumerate over.
|
348
|
+
def search(query)
|
349
|
+
SearchResults.new(self, query)
|
350
|
+
end
|
351
|
+
# @!endgroup
|
352
|
+
|
353
|
+
# An enumerator with a query for searching an Orchestrate::Collection
|
354
|
+
class SearchResults
|
355
|
+
# @return [Collection] The collection this object will search.
|
356
|
+
attr_reader :collection
|
357
|
+
|
358
|
+
# @return [#to_s] The Lucene Query String given as the search query.
|
359
|
+
attr_reader :query
|
360
|
+
|
361
|
+
# @return [Integer] The number of results to fetch per request.
|
362
|
+
attr_reader :limit
|
363
|
+
|
364
|
+
# Initialize a new SearchResults object
|
365
|
+
# @param collection [Orchestrate::Collection] The collection to search.
|
366
|
+
# @param query [#to_s] The Lucene Query to perform.
|
367
|
+
def initialize(collection, query)
|
368
|
+
@collection = collection
|
369
|
+
@query = query
|
370
|
+
@limit = 100
|
371
|
+
@offset= nil
|
372
|
+
end
|
373
|
+
|
374
|
+
include Enumerable
|
375
|
+
|
376
|
+
# Iterates over each result from the Search. Used as the basis for Enumerable methods. Items are
|
377
|
+
# provided on the basis of score, with most relevant first.
|
378
|
+
# @yieldparam [Array<(Float, Orchestrate::KeyValue)>] score,key_value The item's score and the item.
|
379
|
+
# @example
|
380
|
+
# collection.search("trendy").take(5).each do |score, item|
|
381
|
+
# puts "#{item.key} has a trend score of #{score}"
|
382
|
+
# end
|
383
|
+
def each
|
384
|
+
params = {limit:limit}
|
385
|
+
params[:offset] = offset if offset
|
386
|
+
@response ||= collection.app.client.search(collection.name, query, params)
|
387
|
+
return enum_for(:each) unless block_given?
|
388
|
+
raise ResultsNotReady.new if collection.app.inside_parallel?
|
389
|
+
loop do
|
390
|
+
@response.results.each do |listing|
|
391
|
+
yield [ listing['score'], KeyValue.from_listing(collection, listing, @response) ]
|
392
|
+
end
|
393
|
+
break unless @response.next_link
|
394
|
+
@response = @response.next_results
|
395
|
+
end
|
396
|
+
@response = nil
|
397
|
+
end
|
398
|
+
|
399
|
+
# Creates a Lazy Enumerator for the Search Results. If called inside its
|
400
|
+
# app's `in_parallel` block, will pre-fetch results.
|
401
|
+
def lazy
|
402
|
+
return each.lazy if collection.app.inside_parallel?
|
403
|
+
super
|
404
|
+
end
|
405
|
+
|
406
|
+
# Returns the first n items. Equivalent to Enumerable#take. Sets the
|
407
|
+
# `limit` parameter on the query to Orchestrate, so we don't ask for more than is needed.
|
408
|
+
# @param count [Integer] The number of items to limit to.
|
409
|
+
# @return [Array]
|
410
|
+
def take(count)
|
411
|
+
@limit = count > 100 ? 100 : count
|
412
|
+
super(count)
|
413
|
+
end
|
306
414
|
|
415
|
+
# Sets the offset for the query to Orchestrate, so you can skip items. Does not fire a request.
|
416
|
+
# Impelemented as separate method from drop, unlike #take, because take is a more common use case.
|
417
|
+
# @overload offset
|
418
|
+
# @return [Integer, nil] The number of items to skip. Nil is equivalent to zero.
|
419
|
+
# @overload offset(conunt)
|
420
|
+
# @param count [Integer] The number of items to skip.
|
421
|
+
# @return [SearchResults] self.
|
422
|
+
def offset(count=nil)
|
423
|
+
if count
|
424
|
+
@offset = count
|
425
|
+
self
|
426
|
+
else
|
427
|
+
@offset
|
428
|
+
end
|
429
|
+
end
|
307
430
|
end
|
308
431
|
|
309
432
|
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Orchestrate
|
2
|
+
|
3
|
+
# Manages graph relationships for a KeyValue item.
|
4
|
+
class Graph
|
5
|
+
|
6
|
+
# Instantiates a new Graph manager.
|
7
|
+
# @param kv_item [Orchestrate::KeyValue] The KeyValue item on the starting end of the graph.
|
8
|
+
def initialize(kv_item)
|
9
|
+
@kv_item = kv_item
|
10
|
+
@types = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Accessor for graph relation types.
|
14
|
+
# @return [RelationStem]
|
15
|
+
def [](relation_type)
|
16
|
+
@types[relation_type.to_s] || RelationStem.new(@kv_item, relation_type.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
# A directed relationship against a single KeyValue object.
|
20
|
+
class RelationStem
|
21
|
+
|
22
|
+
# the KeyValue object this RelationStem acts on behalf of.
|
23
|
+
# @return [Orchestrate::KeyValue]
|
24
|
+
attr_accessor :kv_item
|
25
|
+
|
26
|
+
# the type of relation this RelationStem interacts with.
|
27
|
+
# @return [String]
|
28
|
+
attr_accessor :type
|
29
|
+
|
30
|
+
# Instantiates a new RelationStem
|
31
|
+
# @param kv_item [Orchestrate::KeyValue] the KeyValue object this RelationStem acts on behalf of.
|
32
|
+
# @param type_name [#to_s] the type of relation this RelationStem interacts with.
|
33
|
+
def initialize(kv_item, type_name)
|
34
|
+
@kv_item = kv_item
|
35
|
+
@client = kv_item.collection.app.client
|
36
|
+
@type = type_name.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
# [Creates a relationship between two objects](http://orchestrate.io/docs/api/#graph/put).
|
40
|
+
# Relations can span collections.
|
41
|
+
# @overload <<(key_value_item)
|
42
|
+
# @param key_value_item [Orchestrate::KeyValue] The KeyValue item to create the relationship with.
|
43
|
+
# @overload <<(collection_name, key_name)
|
44
|
+
# @param collection_name [#to_s] The collection which the other item belongs to.
|
45
|
+
# @param key_name [#to_s] The key of the other item.
|
46
|
+
# @return [Orchestrate::API::Response]
|
47
|
+
def <<(other_item_or_collection_name, other_key=nil)
|
48
|
+
coll, key = get_collection_and_key(kv_item, nil)
|
49
|
+
other_collection, other_key = get_collection_and_key(other_item_or_collection_name, other_key)
|
50
|
+
@client.put_relation(coll, key, type, other_collection, other_key)
|
51
|
+
end
|
52
|
+
alias :push :<<
|
53
|
+
|
54
|
+
# [Deletes a relationship between two objects](http://orchestrate.io/docs/api/#graph/delete29).
|
55
|
+
# @overload delete(key_value_item)
|
56
|
+
# @param key_value_item [Orchestrate::KeyValue] The KeyValue item to create the relationship with.
|
57
|
+
# @overload delete(collection_name, key_name)
|
58
|
+
# @param collection_name [#to_s] The collection which the other item belongs to.
|
59
|
+
# @param key_name [#to_s] The key of the other item.
|
60
|
+
# @return [Orchestrate::API::Response]
|
61
|
+
def delete(other_item_or_collection_name, other_key=nil)
|
62
|
+
coll, key = get_collection_and_key(kv_item, nil)
|
63
|
+
other_collection, other_key = get_collection_and_key(other_item_or_collection_name, other_key)
|
64
|
+
@client.delete_relation(coll, key, type, other_collection, other_key)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Adds depth to the retrieval of related items.
|
68
|
+
# @param type_n [#to_s] The kind of the relation for the second layer of depth to retreive results for.
|
69
|
+
# @return [Traversal]
|
70
|
+
def [](type_n)
|
71
|
+
Traversal.new(kv_item, [type, type_n.to_s])
|
72
|
+
end
|
73
|
+
|
74
|
+
include Enumerable
|
75
|
+
|
76
|
+
# [Retrieves the related items](http://orchestrate.io/api/graph), and
|
77
|
+
# iterates over each item in the result. Used as the basis for
|
78
|
+
# Enumerable methods.
|
79
|
+
# @overload each
|
80
|
+
# @return [Enumerator]
|
81
|
+
# @overload each(&block)
|
82
|
+
# @yieldparam [Orchestrate::KeyValue] key_value The KeyValue item
|
83
|
+
def each(&block)
|
84
|
+
Traversal.new(kv_item, [type]).each(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def get_collection_and_key(item_or_collection, key)
|
89
|
+
if item_or_collection.kind_of?(KeyValue)
|
90
|
+
collection = item_or_collection.collection_name
|
91
|
+
key = item_or_collection.key
|
92
|
+
else
|
93
|
+
collection = item_or_collection
|
94
|
+
end
|
95
|
+
[collection, key]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Traverses from a single node in the graph across one or more edges.
|
99
|
+
# The workhorse for gathering results from a graph search.
|
100
|
+
class Traversal
|
101
|
+
|
102
|
+
# The KeyValue item from which the graph query originates.
|
103
|
+
# @return [KeyValue]
|
104
|
+
attr_accessor :kv_item
|
105
|
+
|
106
|
+
# The graph types and depth to traverse.
|
107
|
+
# @return [Array<#to_s>]
|
108
|
+
attr_accessor :edges
|
109
|
+
|
110
|
+
# Instantiates a new Traversal.
|
111
|
+
# @param kv_item [KeyValue] The KeyValue item from which to traverse.
|
112
|
+
# @param edge_names [Array<#to_s>] The graph types and depth to traverse.
|
113
|
+
def initialize(kv_item, edge_names)
|
114
|
+
@kv_item = kv_item
|
115
|
+
@edges = edge_names
|
116
|
+
@client = kv_item.collection.app.client
|
117
|
+
end
|
118
|
+
|
119
|
+
# Add a new type to the depth of the graph query.
|
120
|
+
# @param edge [#to_s] The type of relation to traverse.
|
121
|
+
# @return [Traversal]
|
122
|
+
def [](edge)
|
123
|
+
self.class.new(kv_item, [edges, edge].flatten)
|
124
|
+
end
|
125
|
+
|
126
|
+
include Enumerable
|
127
|
+
|
128
|
+
# [Retrieves the related items](http://orchestrate.io/api/graph), and
|
129
|
+
# iterates over each item in the result. Used as the basis for
|
130
|
+
# Enumerable methods.
|
131
|
+
def each(&block)
|
132
|
+
@response = @client.get_relations(kv_item.collection_name, kv_item.key, *edges)
|
133
|
+
return enum_for(:each) unless block
|
134
|
+
raise ResultsNotReady if @client.http.parallel_manager
|
135
|
+
@response.results.each do |listing|
|
136
|
+
listing_collection = kv_item.collection.app[listing['path']['collection']]
|
137
|
+
yield KeyValue.from_listing(listing_collection, listing, @response)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
@@ -26,6 +26,25 @@ module Orchestrate
|
|
26
26
|
kv
|
27
27
|
end
|
28
28
|
|
29
|
+
# Instantiate a KeyValue from a listing in a LIST or SEARCH result
|
30
|
+
# @param collection [Orchestrate::Collection] The collection to which the KeyValue belongs.
|
31
|
+
# @param listing [Hash] The entry in the LIST or SEARCH result
|
32
|
+
# @option listing [Hash] path **required** The path of the entry, with collection, key and ref keys.
|
33
|
+
# @option listing [Hash] value **required** The value for the entry
|
34
|
+
# @option listing [Time] reftime The time which the ref was created (only returned by LIST)
|
35
|
+
# @param response [Orchestrate::API::Response] The response which the listing came from
|
36
|
+
# @return Orchestrate::KeyValue The KeyValue item.
|
37
|
+
def self.from_listing(collection, listing, response)
|
38
|
+
path = listing.fetch('path')
|
39
|
+
key = path.fetch('key')
|
40
|
+
ref = path.fetch('ref')
|
41
|
+
kv = new(collection, key, response)
|
42
|
+
kv.instance_variable_set(:@ref, ref)
|
43
|
+
kv.instance_variable_set(:@reftime, listing['reftime']) if listing['reftime']
|
44
|
+
kv.value = listing.fetch('value')
|
45
|
+
kv
|
46
|
+
end
|
47
|
+
|
29
48
|
# The collection this KeyValue belongs to.
|
30
49
|
# @return [Orchestrate::Collection]
|
31
50
|
attr_reader :collection
|
@@ -66,28 +85,19 @@ module Orchestrate
|
|
66
85
|
# Instantiates a new KeyValue item. You generally don't want to call this yourself, but rather use
|
67
86
|
# the methods on Orchestrate::Collection to load a KeyValue.
|
68
87
|
# @param coll [Orchestrate::Collection] The collection to which this KeyValue belongs.
|
69
|
-
# @param
|
70
|
-
# @param
|
71
|
-
#
|
72
|
-
# If key_name_or_listing is a listing, and this value is a Time, used to set last_request_time.
|
73
|
-
# Otherwise if an API::Request, used to load attributes and value.
|
88
|
+
# @param key_name [#to_s] The name of the key
|
89
|
+
# @param associated_response [nil, Orchestrate::API::Response]
|
90
|
+
# If an API::Request, used to load attributes and value.
|
74
91
|
# @return Orchestrate::KeyValue
|
75
|
-
def initialize(coll,
|
92
|
+
def initialize(coll, key_name, associated_response=nil)
|
76
93
|
@collection = coll
|
77
94
|
@collection_name = coll.name
|
78
95
|
@app = coll.app
|
79
|
-
|
80
|
-
path = key_name_or_listing.fetch('path')
|
81
|
-
@key = path.fetch('key')
|
82
|
-
@ref = path.fetch('ref')
|
83
|
-
@reftime = Time.at(key_name_or_listing.fetch('reftime') / 1000.0)
|
84
|
-
@value = key_name_or_listing.fetch('value')
|
85
|
-
@last_request_time = response_or_request_time if response_or_request_time.kind_of?(Time)
|
86
|
-
else
|
87
|
-
@key = key_name_or_listing.to_s
|
88
|
-
end
|
96
|
+
@key = key_name.to_s
|
89
97
|
@id = "#{collection_name}/#{key}"
|
90
|
-
|
98
|
+
@value = {}
|
99
|
+
@ref = false
|
100
|
+
load_from_response(associated_response) if associated_response
|
91
101
|
end
|
92
102
|
|
93
103
|
# Equivalent to `String#==`. Compares by key and collection.
|
@@ -177,6 +187,27 @@ module Orchestrate
|
|
177
187
|
end
|
178
188
|
end
|
179
189
|
|
190
|
+
# Merges a set of values into the item's existing value and saves.
|
191
|
+
# @param merge [#each_pair] The Hash-like to merge into #value. Keys will be stringified.
|
192
|
+
# @return [true, false]
|
193
|
+
def update(merge)
|
194
|
+
begin
|
195
|
+
update!(merge)
|
196
|
+
rescue API::RequestError, API::ServiceError
|
197
|
+
false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Merges a set of values into the item's existing value and saves.
|
202
|
+
# @param merge [#each_pair] The Hash-like to merge into #value. Keys will be stringified.
|
203
|
+
# @return [true]
|
204
|
+
# @raise [Orchestrate::API::VersionMismatch] If the KeyValue item has been updated with a new ref since this KeyValue was loaded.
|
205
|
+
# @raise [Orchestrate::API::RequestError, Orchestrate::API::ServiceError] If there was any other problem with saving.
|
206
|
+
def update!(merge)
|
207
|
+
merge.each_pair {|key, value| @value[key.to_s] = value }
|
208
|
+
save!
|
209
|
+
end
|
210
|
+
|
180
211
|
# Deletes the KeyValue item from Orchestrate using 'If-Match' with the current ref.
|
181
212
|
# Returns false if the item failed to delete because a new ref had been created since this KeyValue was loaded.
|
182
213
|
# @return [true, false]
|
@@ -217,12 +248,22 @@ module Orchestrate
|
|
217
248
|
true
|
218
249
|
end
|
219
250
|
|
220
|
-
# @!
|
251
|
+
# @!endgroup persistence
|
252
|
+
#
|
253
|
+
# @!group relations
|
254
|
+
|
255
|
+
# Entry point for managing the graph relationships for this KeyValue item
|
256
|
+
# @return [Orchestrate::Graph] A graph instance bound to this item.
|
257
|
+
def relations
|
258
|
+
@relations ||= Graph.new(self)
|
259
|
+
end
|
260
|
+
|
261
|
+
# @!endgroup relations
|
221
262
|
|
222
263
|
private
|
223
264
|
def load_from_response(response)
|
224
265
|
response.on_complete do
|
225
|
-
@ref = response.ref
|
266
|
+
@ref = response.ref if response.respond_to?(:ref)
|
226
267
|
@value = response.body unless response.body.respond_to?(:strip) && response.body.strip.empty?
|
227
268
|
@last_request_time = response.request_time
|
228
269
|
end
|
data/lib/orchestrate/version.rb
CHANGED
data/lib/orchestrate.rb
CHANGED
@@ -41,4 +41,14 @@ class ApplicationTest < MiniTest::Unit::TestCase
|
|
41
41
|
assert_equal 'users', users.name
|
42
42
|
end
|
43
43
|
|
44
|
+
def dup_generates_new_client
|
45
|
+
client, stubs = make_client_and_artifacts
|
46
|
+
app = Orchestrate::Application.new(client)
|
47
|
+
duped_app = app.dup
|
48
|
+
assert_equal app.api_key, duped_app.api_key
|
49
|
+
assert_equal client.faraday_configuration, duped_app.client.faraday_configuration
|
50
|
+
refute_equal client.object_id, duped_app.client.object_id
|
51
|
+
refute_equal client.http.object_id, duped_app.client.http.object_id
|
52
|
+
end
|
53
|
+
|
44
54
|
end
|
@@ -47,4 +47,21 @@ class ClientTest < MiniTest::Unit::TestCase
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
+
def test_escapes_url_segments
|
51
|
+
client, stubs = make_client_and_artifacts
|
52
|
+
stubs.get('/v0/foo%2Fbars/(bar%20baz)') { [200, response_headers, {}.to_json] }
|
53
|
+
client.get("foo/bars", "(bar baz)")
|
54
|
+
stubs.verify_stubbed_calls
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_dup
|
58
|
+
client = Orchestrate::Client.new('8c3') do |f|
|
59
|
+
f.adapter :test
|
60
|
+
end
|
61
|
+
dup_client = client.dup
|
62
|
+
assert_equal client.api_key, dup_client.api_key
|
63
|
+
assert_equal client.faraday_configuration, dup_client.faraday_configuration
|
64
|
+
refute_equal client.http.object_id, dup_client.http.object_id
|
65
|
+
end
|
66
|
+
|
50
67
|
end
|
@@ -2,10 +2,13 @@ require "test_helper"
|
|
2
2
|
|
3
3
|
class CollectionEnumerationTest < MiniTest::Unit::TestCase
|
4
4
|
|
5
|
-
def
|
6
|
-
app, stubs = make_application
|
7
|
-
|
8
|
-
|
5
|
+
def setup
|
6
|
+
@app, @stubs = make_application({parallel: true})
|
7
|
+
@called = false
|
8
|
+
@limit = "100"
|
9
|
+
@stubs.get("/v0/items") do |env|
|
10
|
+
@called = true
|
11
|
+
assert_equal @limit, env.params['limit']
|
9
12
|
body = case env.params['afterKey']
|
10
13
|
when nil
|
11
14
|
{ "results" => 100.times.map {|x| make_kv_listing(:items, key: "key-#{x}") },
|
@@ -18,7 +21,10 @@ class CollectionEnumerationTest < MiniTest::Unit::TestCase
|
|
18
21
|
end
|
19
22
|
[ 200, response_headers, body.to_json ]
|
20
23
|
end
|
21
|
-
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_enumerates_over_all
|
27
|
+
items = @app[:items].map {|item| item }
|
22
28
|
assert_equal 104, items.length
|
23
29
|
items.each_with_index do |item, index|
|
24
30
|
assert_equal "key-#{index}", item.key
|
@@ -30,39 +36,76 @@ class CollectionEnumerationTest < MiniTest::Unit::TestCase
|
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
33
|
-
def
|
34
|
-
|
35
|
-
stubs.get("/v0/items") do |env|
|
36
|
-
body = case env.params['afterKey']
|
37
|
-
when nil
|
38
|
-
{ "results" => 10.times.map {|x| make_kv_listing(:items, key: "key-#{x}") },
|
39
|
-
"next" => "/v0/items?afterKey=key-9", "count" => 10 }
|
40
|
-
when 'key-9'
|
41
|
-
{ "results" => 4.times.map {|x| make_kv_listing(:items, key: "key-#{10+x}")},
|
42
|
-
"count" => 4 }
|
43
|
-
else
|
44
|
-
raise ArgumentError.new("unexpected afterKey: #{env.params['afterKey']}")
|
45
|
-
end
|
46
|
-
[ 200, response_headers, body.to_json ]
|
47
|
-
end
|
48
|
-
items=nil
|
39
|
+
def test_enumerator_in_parallel_raises_not_ready_if_forced
|
40
|
+
@limit = "5"
|
49
41
|
assert_raises Orchestrate::ResultsNotReady do
|
50
|
-
app.in_parallel { app[:items].take(5) }
|
42
|
+
@app.in_parallel { @app[:items].take(5) }
|
51
43
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
items.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_enumerator_in_parallel_prefetches_lazy_enums
|
47
|
+
return unless [].respond_to?(:lazy)
|
48
|
+
items = nil
|
49
|
+
@app.in_parallel do
|
50
|
+
items = @app[:items].lazy.map {|item| item }
|
51
|
+
end
|
52
|
+
assert @called, "lazy enumerator in parallel was not prefetched"
|
53
|
+
items = items.force
|
54
|
+
assert_equal 104, items.length
|
55
|
+
items.each_with_index do |item, index|
|
56
|
+
assert_equal "key-#{index}", item.key
|
57
|
+
assert item.ref
|
58
|
+
assert item.reftime
|
59
|
+
assert item.value
|
60
|
+
assert_equal "key-#{index}", item[:key]
|
61
|
+
assert_in_delta Time.now.to_f, item.last_request_time.to_f, 1.1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_enumerator_in_parallel_fetches_enums
|
66
|
+
items = nil
|
67
|
+
@app.in_parallel do
|
68
|
+
items = @app[:items].each
|
69
|
+
end
|
70
|
+
assert @called, "enumerator wasn't prefetched inside of parallel"
|
71
|
+
assert_equal 104, items.to_a.size
|
72
|
+
items.each_with_index do |item, index|
|
73
|
+
assert_equal "key-#{index}", item.key
|
74
|
+
assert item.ref
|
75
|
+
assert item.reftime
|
76
|
+
assert item.value
|
77
|
+
assert_equal "key-#{index}", item[:key]
|
78
|
+
assert_in_delta Time.now.to_f, item.last_request_time.to_f, 1.1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_enumerator_doesnt_prefetch_lazy_enums
|
83
|
+
return unless [].respond_to?(:lazy)
|
84
|
+
items = @app[:items].lazy.map {|item| item }
|
85
|
+
refute @called, "lazy enumerator was prefetched outside of parallel"
|
86
|
+
items = items.force
|
87
|
+
assert_equal 104, items.length
|
88
|
+
items.each_with_index do |item, index|
|
89
|
+
assert_equal "key-#{index}", item.key
|
90
|
+
assert item.ref
|
91
|
+
assert item.reftime
|
92
|
+
assert item.value
|
93
|
+
assert_equal "key-#{index}", item[:key]
|
94
|
+
assert_in_delta Time.now.to_f, item.last_request_time.to_f, 1.1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_enumerator_prefetches_enums
|
99
|
+
items = @app[:items].each
|
100
|
+
assert @called, "enumerator was not prefetched"
|
101
|
+
assert_equal 104, items.to_a.size
|
102
|
+
items.each_with_index do |item, index|
|
103
|
+
assert_equal "key-#{index}", item.key
|
104
|
+
assert item.ref
|
105
|
+
assert item.reftime
|
106
|
+
assert item.value
|
107
|
+
assert_equal "key-#{index}", item[:key]
|
108
|
+
assert_in_delta Time.now.to_f, item.last_request_time.to_f, 1.1
|
66
109
|
end
|
67
110
|
end
|
68
111
|
|
@@ -22,6 +22,22 @@ class Collection_KV_Accessors_Test < MiniTest::Unit::TestCase
|
|
22
22
|
assert_nil @items[:absent]
|
23
23
|
end
|
24
24
|
|
25
|
+
def test_kv_builder
|
26
|
+
body = {"hello" => "nothing"}
|
27
|
+
kv = @items.build(:absent, body)
|
28
|
+
assert_equal 'absent', kv.key
|
29
|
+
assert_equal body, kv.value
|
30
|
+
assert_equal false, kv.ref
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_kv_stubber
|
34
|
+
kv = @items.stub(:hello)
|
35
|
+
assert_equal 'hello', kv.key
|
36
|
+
assert_equal false, kv.ref
|
37
|
+
assert_nil kv.value
|
38
|
+
refute kv.loaded?
|
39
|
+
end
|
40
|
+
|
25
41
|
def test_kv_setter
|
26
42
|
body = { "hello" => "world" }
|
27
43
|
ref = nil
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class CollectionSearchingTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@app, @stubs = make_application({parallel:true})
|
6
|
+
@items = @app[:items]
|
7
|
+
|
8
|
+
@query = "foo"
|
9
|
+
@limit = 100
|
10
|
+
@total = 110
|
11
|
+
|
12
|
+
@make_listing = lambda{|i| make_kv_listing(:items, key: "item-#{i}", reftime: nil, score: @total-i/@total*5.0) }
|
13
|
+
@handle_offset = lambda do |offset|
|
14
|
+
case offset
|
15
|
+
when nil
|
16
|
+
{ "results" => 100.times.map{|i| @make_listing.call(i)}, "count" => 100, "total_count" => @total,
|
17
|
+
"next" => "/v0/items?query=foo&offset=100&limit=100"}
|
18
|
+
when "100"
|
19
|
+
{ "results" => 10.times.map{|i| @make_listing.call(i+100)}, "count" => 10, "total_count" => @total }
|
20
|
+
else
|
21
|
+
raise ArgumentError.new("unexpected offset: #{env.params['offset']}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@called = false
|
26
|
+
@stubs.get("/v0/items") do |env|
|
27
|
+
@called = true
|
28
|
+
assert_equal @query, env.params['query']
|
29
|
+
assert_equal @limit, env.params['limit'].to_i
|
30
|
+
body = @handle_offset.call(env.params['offset'])
|
31
|
+
[ 200, response_headers, body.to_json ]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_basic_search
|
36
|
+
results = @items.search("foo").map{|i| i }
|
37
|
+
assert_equal 110, results.length
|
38
|
+
results.each_with_index do |item, idx|
|
39
|
+
assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
|
40
|
+
assert_equal "item-#{idx}", item[1].key
|
41
|
+
assert_nil item[1].reftime
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_basic_as_needed
|
46
|
+
@limit = 50
|
47
|
+
offset = 10
|
48
|
+
@handle_offset = lambda do |o|
|
49
|
+
case o
|
50
|
+
when "10"
|
51
|
+
{ "results" => 50.times.map{|i| @make_listing.call(i+offset) }, "count" => @limit, "total_count" => @total,
|
52
|
+
"next" => "/v0/items?query=foo&offset=#{offset+@limit}&limit=#{@limit}"}
|
53
|
+
else
|
54
|
+
raise ArgumentError.new("unexpected offset: #{o}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
results = @items.search("foo").offset(offset).take(@limit)
|
58
|
+
assert_equal 50, results.length
|
59
|
+
results.each_with_index do |item, idx|
|
60
|
+
assert_in_delta (@total-(idx+10)/@total * 5.0), item[0], 0.005
|
61
|
+
assert_equal "item-#{idx+10}", item[1].key
|
62
|
+
assert_nil item[1].reftime
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_in_parallel_prefetches_enums
|
67
|
+
items = nil
|
68
|
+
@app.in_parallel { items = @app[:items].search("foo").each }
|
69
|
+
assert @called, "enum wasn't prefetched inside in_parallel"
|
70
|
+
assert_equal @total, items.to_a.size
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_in_parallel_prefetches_lazy_enums
|
74
|
+
return unless [].respond_to?(:lazy)
|
75
|
+
items = nil
|
76
|
+
@app.in_parallel { items = @app[:items].search("foo").lazy.map{|d| d } }
|
77
|
+
assert @called, "lazy wasn't prefetched from in_parallel"
|
78
|
+
assert_equal @total, items.force.size
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_in_parallel_raises_if_forced
|
82
|
+
assert_raises Orchestrate::ResultsNotReady do
|
83
|
+
@app.in_parallel { @app[:items].search("foo").to_a }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_enums_prefetch
|
88
|
+
items = nil
|
89
|
+
@app.in_parallel { items = @app[:items].search("foo").each }
|
90
|
+
assert @called, "enum wasn't prefetched"
|
91
|
+
assert_equal @total, items.to_a.size
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_lazy_enums_dont_prefetch
|
95
|
+
return unless [].respond_to?(:lazy)
|
96
|
+
items = @app[:items].search("foo").lazy.map{|d| d }
|
97
|
+
refute @called, "lazy was prefetched outside in_parallel"
|
98
|
+
assert_equal @total, items.force.size
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -4,7 +4,7 @@ class KeyValuePersistenceTest < MiniTest::Unit::TestCase
|
|
4
4
|
def setup
|
5
5
|
@app, @stubs = make_application
|
6
6
|
@items = @app[:items]
|
7
|
-
@kv = make_kv_item(@items, @stubs, :loaded => Time.now - 60)
|
7
|
+
@kv = make_kv_item(@items, @stubs, :loaded => Time.now - 60, body: {"hello" => "world", "another" => "key"})
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_save_performs_put_if_match_and_returns_true_on_success
|
@@ -109,6 +109,68 @@ class KeyValuePersistenceTest < MiniTest::Unit::TestCase
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
+
def test_update_merges_values_and_saves
|
113
|
+
update = {hello: "there", 'two' => 2}
|
114
|
+
body = @kv.value
|
115
|
+
update.each_pair {|key, value| body[key.to_s] = value }
|
116
|
+
new_ref = make_ref
|
117
|
+
@stubs.put("/v0/items/#{@kv.key}") do |env|
|
118
|
+
assert_equal body, JSON.parse(env.body)
|
119
|
+
assert_header 'If-Match', "\"#{@kv.ref}\"", env
|
120
|
+
[201, response_headers({'Etag' => new_ref, "Location" => "/v0/items/#{@kv.key}/refs/#{new_ref}"}), '']
|
121
|
+
end
|
122
|
+
@kv.update(update)
|
123
|
+
assert_equal body, @kv.value
|
124
|
+
assert_equal new_ref, @kv.ref
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_update_merges_values_and_returns_false_on_error
|
128
|
+
update = {hello: "there", 'two' => 2}
|
129
|
+
body = @kv.value
|
130
|
+
update.each_pair {|key, value| body[key.to_s] = value }
|
131
|
+
old_ref = @kv.ref
|
132
|
+
@stubs.put("/v0/items/#{@kv.key}") do |env|
|
133
|
+
assert_equal body, JSON.parse(env.body)
|
134
|
+
assert_header 'If-Match', "\"#{@kv.ref}\"", env
|
135
|
+
error_response(:service_error)
|
136
|
+
end
|
137
|
+
refute @kv.update(update)
|
138
|
+
assert_equal body, @kv.value
|
139
|
+
assert_equal old_ref, @kv.ref
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_update_bang_merges_values_and_save_bangs
|
143
|
+
update = {hello: "there", 'two' => 2}
|
144
|
+
body = @kv.value
|
145
|
+
update.each_pair {|key, value| body[key.to_s] = value }
|
146
|
+
new_ref = make_ref
|
147
|
+
@stubs.put("/v0/items/#{@kv.key}") do |env|
|
148
|
+
assert_equal body, JSON.parse(env.body)
|
149
|
+
assert_header 'If-Match', "\"#{@kv.ref}\"", env
|
150
|
+
[201, response_headers({'Etag' => new_ref, "Location" => "/v0/items/#{@kv.key}/refs/#{new_ref}"}), '']
|
151
|
+
end
|
152
|
+
@kv.update!(update)
|
153
|
+
assert_equal body, @kv.value
|
154
|
+
assert_equal new_ref, @kv.ref
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_update_bang_merges_values_and_save_bangs_raises_on_error
|
158
|
+
update = {hello: "there", 'two' => 2}
|
159
|
+
body = @kv.value
|
160
|
+
update.each_pair {|key, value| body[key.to_s] = value }
|
161
|
+
old_ref = @kv.ref
|
162
|
+
@stubs.put("/v0/items/#{@kv.key}") do |env|
|
163
|
+
assert_equal body, JSON.parse(env.body)
|
164
|
+
assert_header 'If-Match', "\"#{@kv.ref}\"", env
|
165
|
+
error_response(:service_error)
|
166
|
+
end
|
167
|
+
assert_raises Orchestrate::API::ServiceError do
|
168
|
+
@kv.update!(update)
|
169
|
+
end
|
170
|
+
assert_equal body, @kv.value
|
171
|
+
assert_equal old_ref, @kv.ref
|
172
|
+
end
|
173
|
+
|
112
174
|
def test_destroy_performs_delete_if_match_and_returns_true_on_success
|
113
175
|
@stubs.delete("/v0/items/#{@kv.key}") do |env|
|
114
176
|
assert_header 'If-Match', "\"#{@kv.ref}\"", env
|
@@ -59,21 +59,11 @@ class KeyValueTest < MiniTest::Unit::TestCase
|
|
59
59
|
kv = Orchestrate::KeyValue.new(items, 'keyname')
|
60
60
|
assert_equal 'keyname', kv.key
|
61
61
|
assert_equal items, kv.collection
|
62
|
+
assert_equal Hash.new, kv.value
|
63
|
+
assert_equal false, kv.ref
|
62
64
|
assert ! kv.loaded?
|
63
65
|
end
|
64
66
|
|
65
|
-
def test_instantiates_from_collection_and_listing
|
66
|
-
app, stubs = make_application
|
67
|
-
listing = make_kv_listing('items', {key: "foo"})
|
68
|
-
kv = Orchestrate::KeyValue.new(app[:items], listing, Time.now)
|
69
|
-
assert_equal 'items', kv.collection_name
|
70
|
-
assert_equal "foo", kv.key
|
71
|
-
assert_equal listing['path']['ref'], kv.ref
|
72
|
-
assert_equal listing['value'], kv.value
|
73
|
-
assert_in_delta Time.at(listing['reftime'] / 1000), kv.reftime, 1.1
|
74
|
-
assert_in_delta Time.now.to_f, kv.last_request_time.to_f, 1.1
|
75
|
-
end
|
76
|
-
|
77
67
|
def test_equality
|
78
68
|
app, stubs = make_application
|
79
69
|
app2, stubs = make_application
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class RelationsTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@app, @stubs = make_application
|
7
|
+
@items = @app[:items]
|
8
|
+
@kv = make_kv_item(@items, @stubs)
|
9
|
+
@other_kv = make_kv_item(@items, @stubs)
|
10
|
+
@relation_type = :siblings
|
11
|
+
@path = ['items', @kv.key, 'relation', @relation_type, :items, @other_kv.key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_results(colls, count)
|
15
|
+
count.times.map do |i|
|
16
|
+
type = colls[i % colls.length]
|
17
|
+
make_kv_listing(type, {reftime:nil, key: "#{type.name}-#{i}", body: {index: i}})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_adds_relation_with_kv
|
22
|
+
@stubs.put("/v0/#{@path.join('/')}") { [ 204, response_headers, '' ] }
|
23
|
+
@kv.relations[@relation_type] << @other_kv
|
24
|
+
@stubs.verify_stubbed_calls
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_adds_from_collname_and_keyname
|
28
|
+
@stubs.put("/v0/#{@path.join('/')}") { [ 204, response_headers, '' ] }
|
29
|
+
@kv.relations[@relation_type].push(:items, @other_kv.key)
|
30
|
+
@stubs.verify_stubbed_calls
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_removes_relation_with_kv
|
34
|
+
@stubs.delete("/v0/#{@path.join('/')}") do |env|
|
35
|
+
assert_equal 'true', env.params['purge']
|
36
|
+
[ 204, response_headers, '' ]
|
37
|
+
end
|
38
|
+
@kv.relations[@relation_type].delete(@other_kv)
|
39
|
+
@stubs.verify_stubbed_calls
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_removes_relation_with_collname_and_keyname
|
43
|
+
@stubs.delete("/v0/#{@path.join('/')}") do |env|
|
44
|
+
assert_equal 'true', env.params['purge']
|
45
|
+
[ 204, response_headers, '' ]
|
46
|
+
end
|
47
|
+
@kv.relations[@relation_type].delete(:items, @other_kv.key)
|
48
|
+
@stubs.verify_stubbed_calls
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_enumerates_over_relation
|
52
|
+
colls = [@items, @app[:things], @app[:stuff]]
|
53
|
+
@stubs.get("/v0/#{@kv.collection_name}/#{@kv.key}/relations/#{@relation_type}") do |env|
|
54
|
+
[200, response_headers, {results: make_results(colls, 100), count: 100}.to_json]
|
55
|
+
end
|
56
|
+
related_stuff = @kv.relations[@relation_type].to_a
|
57
|
+
assert_equal 100, related_stuff.size
|
58
|
+
related_stuff.each do |item|
|
59
|
+
assert_kind_of Orchestrate::KeyValue, item
|
60
|
+
match = item.key.match(/(\w+)-(\d+)/)
|
61
|
+
assert_equal item[:index], match[2].to_i
|
62
|
+
assert_equal item.collection.name, match[1]
|
63
|
+
assert_equal item.collection, colls[match[2].to_i % colls.length]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_enumerates_over_subrelation
|
68
|
+
colls = [@items, @app[:things], @app[:stuff]]
|
69
|
+
@stubs.get("/v0/items/#{@kv.key}/relations/siblings/children/siblings") do |env|
|
70
|
+
[200, response_headers, {results: make_results(colls, 100), count:100}.to_json]
|
71
|
+
end
|
72
|
+
related_stuff = @kv.relations[:siblings][:children][:siblings].to_a
|
73
|
+
assert_equal 100, related_stuff.size
|
74
|
+
related_stuff.each do |item|
|
75
|
+
assert_kind_of Orchestrate::KeyValue, item
|
76
|
+
match = item.key.match(/(\w+)-(\d+)/)
|
77
|
+
assert_equal item[:index], match[2].to_i
|
78
|
+
assert_equal item.collection.name, match[1]
|
79
|
+
assert_equal item.collection, colls[match[2].to_i % colls.length]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_prevents_parallel_enumeration
|
84
|
+
app, stubs = make_application({parallel: true})
|
85
|
+
one = make_kv_item(app[:items], stubs, {key: 'one'})
|
86
|
+
stubs.get("/v0/items/one/relations/siblings") do |env|
|
87
|
+
[200, response_headers, {results: make_results([app[:items]], 10), count:10}.to_json]
|
88
|
+
end
|
89
|
+
assert_raises Orchestrate::ResultsNotReady do
|
90
|
+
app.in_parallel { one.relations[:siblings].to_a }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_parallel_get_performs_call
|
95
|
+
called = false
|
96
|
+
siblings = nil
|
97
|
+
app, stubs = make_application({parallel: true})
|
98
|
+
one = make_kv_item(app[:items], stubs, {key: 'one'})
|
99
|
+
stubs.get("/v0/items/one/relations/siblings") do |env|
|
100
|
+
called = true
|
101
|
+
[200, response_headers, {results: make_results([app[:items]], 10), count:10}.to_json]
|
102
|
+
end
|
103
|
+
app.in_parallel do
|
104
|
+
siblings = one.relations[:siblings].each
|
105
|
+
end
|
106
|
+
assert called, "parallel block finished, API call not made yet for enumerable"
|
107
|
+
siblings = siblings.to_a
|
108
|
+
assert_equal 10, siblings.size
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_lazy_does_not_perform_call
|
112
|
+
return unless [].respond_to?(:lazy)
|
113
|
+
called = false
|
114
|
+
app, stubs = make_application
|
115
|
+
one = make_kv_item(app[:items], stubs, {key:'one'})
|
116
|
+
stubs.get("/v0/items/one/relations/siblings") do |env|
|
117
|
+
called = true
|
118
|
+
[200, response_headers, {results: make_results([app[:items]], 10), count:10}.to_json]
|
119
|
+
end
|
120
|
+
siblings = one.relations[:siblings].lazy
|
121
|
+
refute called, "lazy enumerator not evauluated, but request called"
|
122
|
+
siblings = siblings.to_a
|
123
|
+
assert called, "lazy enumerator forced, but request not called"
|
124
|
+
assert_equal 10, siblings.size
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
data/test/test_helper.rb
CHANGED
@@ -93,10 +93,14 @@ end
|
|
93
93
|
def make_kv_listing(collection, opts={})
|
94
94
|
key = opts[:key] || "item-#{rand(1_000_000)}"
|
95
95
|
ref = opts[:ref] || make_ref
|
96
|
-
reftime = opts
|
96
|
+
reftime = opts.fetch(:reftime, Time.now.to_f - (rand(24) * 3600_000))
|
97
|
+
score = opts[:score]
|
97
98
|
body = opts[:body] || {"key" => key}
|
98
|
-
|
99
|
-
|
99
|
+
collection = collection.name if collection.kind_of?(Orchestrate::Collection)
|
100
|
+
result = { "path" => { "collection" => collection, "key" => key, "ref" => ref }, "value" => body }
|
101
|
+
result["reftime"] = reftime if reftime
|
102
|
+
result["score"] = score if score
|
103
|
+
result
|
100
104
|
end
|
101
105
|
|
102
106
|
def capture_warnings
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: orchestrate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Lyon
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2014-07-
|
13
|
+
date: 2014-07-24 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: faraday
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- lib/orchestrate/application.rb
|
135
135
|
- lib/orchestrate/client.rb
|
136
136
|
- lib/orchestrate/collection.rb
|
137
|
+
- lib/orchestrate/graph.rb
|
137
138
|
- lib/orchestrate/key_value.rb
|
138
139
|
- lib/orchestrate/version.rb
|
139
140
|
- orchestrate.gemspec
|
@@ -147,9 +148,11 @@ files:
|
|
147
148
|
- test/orchestrate/client_test.rb
|
148
149
|
- test/orchestrate/collection_enumeration_test.rb
|
149
150
|
- test/orchestrate/collection_kv_accessors_test.rb
|
151
|
+
- test/orchestrate/collection_searching_test.rb
|
150
152
|
- test/orchestrate/collection_test.rb
|
151
153
|
- test/orchestrate/key_value_persistence_test.rb
|
152
154
|
- test/orchestrate/key_value_test.rb
|
155
|
+
- test/orchestrate/relations_test.rb
|
153
156
|
- test/test_helper.rb
|
154
157
|
homepage: https://github.com/orchestrate-io/orchestrate-ruby
|
155
158
|
licenses:
|
@@ -186,8 +189,10 @@ test_files:
|
|
186
189
|
- test/orchestrate/client_test.rb
|
187
190
|
- test/orchestrate/collection_enumeration_test.rb
|
188
191
|
- test/orchestrate/collection_kv_accessors_test.rb
|
192
|
+
- test/orchestrate/collection_searching_test.rb
|
189
193
|
- test/orchestrate/collection_test.rb
|
190
194
|
- test/orchestrate/key_value_persistence_test.rb
|
191
195
|
- test/orchestrate/key_value_test.rb
|
196
|
+
- test/orchestrate/relations_test.rb
|
192
197
|
- test/test_helper.rb
|
193
198
|
has_rdoc:
|