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 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: