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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a997f89d7ead7ddaa902fa36b7c50d5dcaa36fe9
4
- data.tar.gz: e767f9630b2a2c619c22d4606f8005a11e35e079
3
+ metadata.gz: a9ab618bc8f0fd8e7d46d7621b4378a894ec9150
4
+ data.tar.gz: 4869b43cead19ce0c710d135f6551b20e0ef991a
5
5
  SHA512:
6
- metadata.gz: 6a0d8dabd889acb31b465972186862317f3a19d0709d9c96c188fadf06b0cf22748af04821e3331c47111b6f59e8af4390877004bf4a926eb5f2962d89539615
7
- data.tar.gz: c76d14585febf3df794571aa6c2442820c89bdbcd6395ace403bcb7ad714ed8ef120461c7c73b686b5008c7f73643687cb4184446a776ea969554f521a7fc969
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](http://rdoc.info/github/orchestrate-io/orchestrate-ruby/master/frames)
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].lazy.take(100)
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]` enumerator to a lazy enumerator with #lazy. Attempting to access the enumerator's values (for example, without using `#lazy` or by using `#any?` or `#find`) while inside the `in_parallel` block will raise an `Orchestrate::ResultsNotReady` exception.
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
- Lazy Enumerators are not available by default in Ruby 1.9.
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] The API key for your Orchestrate Application.
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
- client.in_parallel(&block)
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.
@@ -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
- # Lazily iterates over each KeyValue item in the collection. Used as the basis for Enumerable methods.
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 = collection.app.client.list(collection.name, params)
287
- raise ResultsNotReady.new if collection.app.client.http.parallel_manager
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.new(collection, doc, response.request_time)
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 key_name_or_listing [Hash] A listing result from Client#list
70
- # @param key_name_or_listing [#to_s] The name of the key
71
- # @param response_or_request_time [nil, Time, Orchestrate::API::Response]
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, key_name_or_listing, response_or_request_time=nil)
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
- if key_name_or_listing.kind_of?(Hash)
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
- load_from_response(response_or_request_time) if response_or_request_time.kind_of?(API::Response)
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
- # @!engroup persistence
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
@@ -1,4 +1,4 @@
1
1
  module Orchestrate
2
2
  # @return [String] The version number of the Orchestrate Gem
3
- VERSION = "0.7.0"
3
+ VERSION = "0.8.0"
4
4
  end
data/lib/orchestrate.rb CHANGED
@@ -3,6 +3,7 @@ require "orchestrate/client"
3
3
  require "orchestrate/application"
4
4
  require "orchestrate/collection"
5
5
  require "orchestrate/key_value"
6
+ require "orchestrate/graph"
6
7
  require "orchestrate/version"
7
8
  #
8
9
  # A library for supporting connections to the \Orchestrate API.
@@ -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 test_enumerates_over_all
6
- app, stubs = make_application
7
- stubs.get("/v0/items") do |env|
8
- assert_equal "100", env.params['limit']
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
- items = app[:items].map {|item| item }
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 test_enumerator_in_parallel
34
- app, stubs = make_application({parallel:true})
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
- if app[:items].respond_to?(:lazy)
53
- app.in_parallel do
54
- items = app[:items].lazy.map {|item| item }
55
- end
56
- items = items.force
57
- assert_equal 14, items.length
58
- items.each_with_index do |item, index|
59
- assert_equal "key-#{index}", item.key
60
- assert item.ref
61
- assert item.reftime
62
- assert item.value
63
- assert_equal "key-#{index}", item[:key]
64
- assert_in_delta Time.now.to_f, item.last_request_time.to_f, 1.1
65
- end
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[:reftime] || Time.now.to_f - (rand(24) * 3600_000)
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
- { "path" => { "collection" => collection, "key" => key, "ref" => ref },
99
- "reftime" => reftime, "value" => body }
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.7.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-01 00:00:00.000000000 Z
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: