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