orchestrate 0.9.2 → 0.10.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: 7ea84118d96674889b76ea3cad03c131e6339aa5
4
- data.tar.gz: d3c664b057f0af8e147060413ab9a43d96b2ae9f
3
+ metadata.gz: f7d5c6bc09a49e08d464387e25b98ae9777bbf93
4
+ data.tar.gz: 72a68c6e456c4920c9875e7e3f7108b29bd59d12
5
5
  SHA512:
6
- metadata.gz: 7eb7729c1590a618715870997959990b8d4ac057ab240eb15109d704cb3277ad146c1b92de958c3a6259ce56656b0f1fd273ea86e0b32b4bbec734aa8381ad1e
7
- data.tar.gz: 8a01ed2702ec401ec9d88628e2b2005a6c1511af13bf0596d051c934d7165e95ac19a00a4c27685b8a1a9bff8832e93bd91a94198f2daec14d802a680cef2afb
6
+ metadata.gz: a8a2a5f7124cd557dc4a3ed3a4fc9d71064494e8241ae87409c8fcfd2fe40236eaebd0171e54c5c2ab22bcba03a1e33bb8a81171681a205edb4c153f747ef2a9
7
+ data.tar.gz: 753527cab83aac5537fd178ac976cf704f6c173c63b0b64a2f9c9fdd859c30d18e6d8cd34cb92bcc4b6bb2aa3643e66f47918193d561c65e88b2f591b82f802e
data/README.md CHANGED
@@ -45,6 +45,24 @@ jill.value # { "name" => "Jill", "location" =>
45
45
  jill.save # PUT-If-Match, updates ref
46
46
  ```
47
47
 
48
+ #### Manipulate KeyValues with PATCH
49
+ ```ruby
50
+ jill.merge({location: "Under the Hill"}) # Updates jill by merging given partial value into existing value
51
+
52
+ # Patch Operations
53
+ jill.add('favorite_sibling', 'Jack').update # adds new field/value pair to jill
54
+ jill.remove('favorite_sibling').update # removes given field/value pair from jill
55
+ jill.replace('on_the_hill', false).update # replaces given field with given value
56
+ jill.move('name', 'first_name').update # moves given field's value to new field
57
+ jill.copy('first_name', 'full_name').update # copies given field's value to another field
58
+ jill.increment('age', 1).update # increments given field (must have numeric value) by provided amount
59
+ jill.decrement('years_to_live', 1).update # decrements given field (must have numeric value) by provided amount
60
+ jill.test('full_name', 'Jill').update # tests equality of existing field/value pair with given field/value
61
+
62
+ # Patch Operations can be chained together to perform multiple updates to a KeyValue item
63
+ jill.add('favorite_food', 'Pizza').remove('years_to_live').update()
64
+ ```
65
+
48
66
  #### Searching, Sorting for KeyValues
49
67
  ```ruby
50
68
  users.search("name:Jill") # returns users with name "Jill"
@@ -63,6 +81,21 @@ users.search("location: Portland").order(:name) # returns users in ascending ord
63
81
  users.search("location: Portland").order(:name, :asc, :rank, :desc, :created_at) # :created_at argument defaults to :asc
64
82
  ```
65
83
 
84
+ ### Geo Queries
85
+ ```ruby
86
+ # Create a Collection object
87
+ cafes = app[:cafes]
88
+
89
+ # Find cafes near a given geographic point
90
+ cafes.near(:location, 12.56, 19.443, 4, 'mi') # returns cafes in a 4 mile radius of given latitude, longitude
91
+
92
+ # Sort nearby cafes by distance
93
+ cafes.near(:location, 12.56, 19.443, 4, 'mi').order(:distance) # returns nearby cafes in ascending order (closest to farthest)
94
+
95
+ # Find cafes in a given area using a bounding box
96
+ cafes.in(:location, {north:12.5, east:57, south:12, west:56}) # returns all cafes within specified bounding box
97
+ ```
98
+
66
99
  ### Method Client use
67
100
 
68
101
  #### Create a Client
@@ -84,6 +117,27 @@ client.delete(:users, :jack, jack.ref) # DELETE-If-Match, returns API::Resp
84
117
  client.list(:users) # LIST users, returns API::CollectionResposne
85
118
  ```
86
119
 
120
+ #### Manipulate KeyValues with PATCH
121
+ ```ruby
122
+ # Give a set of operations to manipulate a given key
123
+ # Operations are executed in sequential order
124
+ ops = [
125
+ { "op" => "add", "path" => "nimble", "value" => true }, # adds new field/value pair
126
+ { "op" => "remove", "path" => "nimble" }, # removes given field/value pair
127
+ { "op" => "replace", "path" => "quick", "value" => true }, # replaces given field with given value
128
+ { "op" => "move", "from" => "name", "path" => "first_name" }, # moves given field's value to new field
129
+ { "op" => "copy", "from" => "city", "path" => "home_town" }, # copies given field's value to another field
130
+ { "op" => "inc", "path" => "age", "value" => 1 }, # increment a numeric value at a given path
131
+ { "op" => "inc", "path" => "age", "value" => -1 }, # pass a negative number to decrement a numeric value
132
+ { "op" => "test", "path" => "first_name", "value" => "Jack" }, # tests equality of existing field/value pair with given field/value
133
+ ]
134
+
135
+ client.patch(:users, :jack, ops)
136
+
137
+ # Merge partial values into existing key
138
+ client.patch_merge(:users, :jack, { favorite_food: "Donuts" })
139
+ ```
140
+
87
141
  #### Search Collections
88
142
  ```ruby
89
143
  client.search(:users, "location:Portland") # search 'users' collection for items with a location of 'Portland'
@@ -96,6 +150,29 @@ client.search(:users, "location:Portland", { sort: "value.name:asc" }) # returns
96
150
  client.search(:users, "location:Portland", { sort: "value.name.last:asc,value.name.first:asc" }) # returns items sorted primarily by last name, but whenever two users have an identical last name, the results will be sorted by first name as well.
97
151
  ```
98
152
 
153
+ ### Geo Queries
154
+ ```ruby
155
+ # Find cafes near a given geographic point
156
+
157
+ coords = {
158
+ lat: 12.56,
159
+ lon: 19.443,
160
+ dist: '4mi' # Define size of search radius for NEAR query
161
+ }
162
+ query = "value.location:NEAR:{lat:#{coords.lat} lon:#{coords.lon} dist:#{coords.dist}}"
163
+ client.search(:cafes, query) # returns cafes in a 4 mile radius of given latitude, longitude
164
+
165
+ # Using the previous coords & query,
166
+ # sort results by distance
167
+ client.search(:cafes, query, {
168
+ sort: 'value.location:distance:asc'
169
+ })
170
+
171
+ # Find cafes in a given area using a bounding box
172
+ query = "value:IN:{ north:12.5 east:57 south:12 west:56 }"
173
+ client.search(:cafes, query)
174
+ ```
175
+
99
176
  ### Examples and Documentation
100
177
 
101
178
  There are more examples at [Orchestrate's API Documentation][apidoc] and documentation in the [rdoc][].
@@ -200,6 +277,13 @@ end
200
277
 
201
278
  ## Release Notes
202
279
 
280
+ ### December 11, 2014: release 0.10.0
281
+ - **BACKWARDS-INCOMPATIBLE** Prior `KeyValue#update` & `KeyValue#update!` renamed to `KeyValue#set` & `KeyValue#set!`. `KeyValue#update` now used after `PATCH` operations to fire the request.
282
+ - Implement `Collection#near` & `Collection#in`, allowing `Collection` to perform geo queries.
283
+ - Implement `Client#patch`, `Client#patch_merge`, allowing `Client` to perform subdocument updates through `PATCH` requests.
284
+ - Implement `KeyValue::OperationSet`, allowing a set of `PATCH` operations to be built by `KeyValue` through `KeyValue#add`, `KeyValue#remove`, `KeyValue#replace`, `KeyValue#move`, `KeyValue#copy`, `KeyValue#increment`, `KeyValue#decrement`, & `KeyValue#test`. The `KeyValue::OperationSet` is fired by ending the chain with `KeyValue#update`.
285
+ - Implement `KeyValue#merge`, allowing `KeyValue` to merge partial values into existing keys through `PATCH` requests.
286
+
203
287
  ### November 19, 2014: release 0.9.2
204
288
  - Implement `SearchResults#order`, allowing `Collection` object results to be sorted.
205
289
  - Implement Data Center choice on `Orchestrate::Client` and `Orchestrate::Application`.
@@ -166,6 +166,46 @@ module Orchestrate
166
166
  put collection, key, body, false
167
167
  end
168
168
 
169
+ # Manipulate values associated with a key, without retrieving the key object.
170
+ # Array of operations passed as body, will execute operations on the key sequentially.
171
+ # [Patch](http://orchestrate.io/docs/apiref#keyvalue-patch).
172
+ # @param collection [#to_s] The name of the collection.
173
+ # @param key [#to_s] The name of the key.
174
+ # @param body [#to_json] The value for the key.
175
+ # @param condition [String, false, nil] Conditions for setting the value.
176
+ # If `String`, value used as `If-Match`, value will only be updated if key's current value's ref matches.
177
+ # If `nil` (default), value is set regardless.
178
+ # @return Orchestrate::API::ItemResponse
179
+ # @raise Orchestrate::API::BadRequest the body is not valid JSON.
180
+ # @raise Orchestrate::API::AlreadyPresent the `false` condition was given, but a value already exists for this collection/key combo.
181
+ def patch(collection, key, body, condition=nil)
182
+ headers = {'Content-Type' => 'application/json-patch+json'}
183
+ if condition.is_a?(String)
184
+ headers['If-Match'] = API::Helpers.format_ref(condition)
185
+ end
186
+ send_request :patch, [collection, key], { body: body, headers: headers, response: API::ItemResponse }
187
+ end
188
+
189
+ # Merge field/value pairs into existing key, without retrieving the key object.
190
+ # [Patch](http://orchestrate.io/docs/apiref#keyvalue-patch-merge).
191
+ # If a given field's value is nil, will remove field from existing key on merge.
192
+ # @param collection [#to_s] The name of the collection.
193
+ # @param key [#to_s] The name of the key.
194
+ # @param body [#to_json] The value for the key.
195
+ # @param condition [String, false, nil] Conditions for setting the value.
196
+ # If `String`, value used as `If-Match`, value will only be updated if key's current value's ref matches.
197
+ # If `nil` (default), value is set regardless.
198
+ # @return Orchestrate::API::ItemResponse
199
+ # @raise Orchestrate::API::BadRequest the body is not valid JSON.
200
+ # @raise Orchestrate::API::AlreadyPresent the `false` condition was given, but a value already exists for this collection/key combo.
201
+ def patch_merge(collection, key, body, condition=nil)
202
+ headers = {'Content-Type' => 'application/merge-patch+json'}
203
+ if condition.is_a?(String)
204
+ headers['If-Match'] = API::Helpers.format_ref(condition)
205
+ end
206
+ send_request :patch, [collection, key], { body: body, headers: headers, response: API::ItemResponse }
207
+ end
208
+
169
209
  # [Sets the current value of a key to a null
170
210
  # object](http://orchestrate.io/docs/api/#key/value/delete11).
171
211
  # @param collection [#to_s] The name of the collection.
@@ -404,6 +444,8 @@ module Orchestrate
404
444
  if [:put, :post].include?(method)
405
445
  headers['Content-Type'] = 'application/json'
406
446
  request.body = body.to_json
447
+ elsif [:patch].include?(method)
448
+ request.body = body.to_json
407
449
  end
408
450
  headers.each {|header, value| request[header] = value }
409
451
  end
@@ -357,6 +357,37 @@ module Orchestrate
357
357
  end
358
358
  # @!endgroup
359
359
 
360
+ # @!group Geo Queries
361
+ # Performs a search for items near a specified geographic point in a collection.
362
+ # [Search for items near a geographic point](http://orchestrate.io/docs/apiref#geo-queries-near)
363
+ # @param field [#to_s] The field containing location data (latitude & longitude)
364
+ # @param latitude [Float] The number representing latitude of a geographic point
365
+ # @param longitude [Float] The number representing longitude of a geographic point
366
+ # @param distance [Integer] The number of distance units.
367
+ # @param units [#to_s] Unit of measurement for distance, default to kilometers (km),
368
+ # supported units: km, m, cm, mm, mi, yd, ft, in, nmi
369
+ # @return [SearchResults] A loaded SearchResults object ready to enumerate over.
370
+ def near(field, latitude, longitude, distance, units=nil)
371
+ units ||= 'km'
372
+ query = "#{field}:NEAR:{lat:#{latitude} lon:#{longitude} dist:#{distance}#{units}}"
373
+ SearchResults.new(self, query)
374
+ end
375
+
376
+ # Performs a search for items within a particular area,
377
+ # using a specified bounding box
378
+ # [Search for items in a geographic bounding box](https://orchestrate.io/docs/apiref#geo-queries-bounding)
379
+ # @param field [#to_s] The field containing location data (latitude & longitude)
380
+ # @param box [#to_json] The values to create the bounding box,
381
+ # @example
382
+ # collection.in(:field, {north: 12.5, south: 15, east: 14, west: 3})
383
+ # @return [SearchResults] A loaded SearchResults object ready to enumerate over.
384
+ def in(field, box={})
385
+ box = box.flatten.each_slice(2).map {|dir, val| "#{dir}:#{val}" }.join(" ")
386
+ query = "#{field}:IN:{#{box}}"
387
+ SearchResults.new(self, query)
388
+ end
389
+ # @!endgroup
390
+
360
391
  # An enumerator with a query for searching an Orchestrate::Collection
361
392
  class SearchResults
362
393
  # @return [Collection] The collection this object will search.
@@ -208,9 +208,9 @@ module Orchestrate
208
208
  # Merges a set of values into the item's existing value and saves.
209
209
  # @param merge [#each_pair] The Hash-like to merge into #value. Keys will be stringified.
210
210
  # @return [true, false]
211
- def update(merge)
211
+ def set(merge)
212
212
  begin
213
- update!(merge)
213
+ set!(merge)
214
214
  rescue API::RequestError, API::ServiceError
215
215
  false
216
216
  end
@@ -221,11 +221,168 @@ module Orchestrate
221
221
  # @return [true]
222
222
  # @raise [Orchestrate::API::VersionMismatch] If the KeyValue item has been updated with a new ref since this KeyValue was loaded.
223
223
  # @raise [Orchestrate::API::RequestError, Orchestrate::API::ServiceError] If there was any other problem with saving.
224
- def update!(merge)
224
+ def set!(merge)
225
225
  merge.each_pair {|key, value| @value[key.to_s] = value }
226
226
  save!
227
227
  end
228
228
 
229
+ # @!group patch-merge
230
+
231
+ # Merges the given value into a KeyValue item using a patch request,
232
+ # enabling merges to be peformed without retrieving the KeyValue item.
233
+ # @param value [#to_json] Hash of partial values, to be converted to json,
234
+ # and merged into the KeyValue item.
235
+ # @param ref [String] optional condition, provide ref for 'If-Match' header
236
+ def merge(value, ref=nil)
237
+ self.perform(:patch_merge, value, ref)
238
+ end
239
+
240
+ # @!endgroup patch-merge
241
+ # @!group patch-operations
242
+
243
+ # Manipulate a KeyValue item with a set of operations.
244
+ # Chain a series of operation methods (#add, #remove, etc) to build the set,
245
+ # operations will be executed by Orchestrate in sequential order.
246
+ # @param ref [String] optional condition, provide ref for 'If-Match' header
247
+ # To perform singular operations or a set, add #update to the end of the method chain.
248
+ # @example
249
+ # users['jon-snow'].add(:beard, true).remove(:some_field).update()
250
+ # @raise Orchestrate::API::NotFound if key does not exist
251
+ def update(ref=nil)
252
+ OperationSet.new(self).update(ref)
253
+ end
254
+
255
+ # Adds a new field/value pair to a KeyValue at the designated path (field name).
256
+ # @param path [#to_s] The location of the field to add.
257
+ # @param value [#to_json] The value to assign to the specified field.
258
+ # @return Orchestrate::KeyValue::OperationSet
259
+ def add(path, value)
260
+ OperationSet.new(self).add(path, value)
261
+ end
262
+
263
+ # Removes a field/value pair from a KeyValue item
264
+ # @param path [#to_s] The field to remove.
265
+ # @raise Orchestrate::API::RequestError if field does not exist
266
+ def remove(path)
267
+ OperationSet.new(self).remove(path)
268
+ end
269
+
270
+ # Replaces an existing field's value
271
+ # @param path [#to_s] The field whose value to replace.
272
+ # @param value [#to_json] The value to assign to the specified field.
273
+ # @raise Orchestrate::API::RequestError if field does not exist
274
+ # @return Orchestrate::KeyValue::OperationSet
275
+ def replace(path, value)
276
+ OperationSet.new(self).replace(path, value)
277
+ end
278
+
279
+ # Moves a KeyValue item's field/value pair to a new field (path)
280
+ # @param from_path [#to_s] The field to move.
281
+ # @param to_path [#to_s] The new location of the moved field.
282
+ # @raise Orchestrate::API::RequestError if field does not exist
283
+ # @return Orchestrate::KeyValue::OperationSet
284
+ def move(from_path, to_path)
285
+ OperationSet.new(self).move(from_path, to_path)
286
+ end
287
+
288
+ # Copies a KeyValue item's field value to another field (path)
289
+ # @param from_path [#to_s] The field to copy.
290
+ # @param to_path [#to_s] The target location to copy field/value to.
291
+ # @raise Orchestrate::API::RequestError if field does not exist
292
+ # @return Orchestrate::KeyValue::OperationSet
293
+ def copy(from_path, to_path)
294
+ OperationSet.new(self).copy(from_path, to_path)
295
+ end
296
+
297
+ # Increases a KeyValue item's field number value by given amount
298
+ # @param path [#to_s] The field (with a number value) to increment.
299
+ # @param amount [Integer] The amount to increment the field's value by.
300
+ # @raise Orchestrate::API::RequestError if field does not exist or
301
+ # if existing value is not a number type
302
+ # @return Orchestrate::KeyValue::OperationSet
303
+ def increment(path, amount)
304
+ OperationSet.new(self).increment(path, amount)
305
+ end
306
+ alias :inc :increment
307
+
308
+ # Decreases a KeyValue item's field number value by given amount
309
+ # @param path [#to_s] The field (with a number value) to decrement.
310
+ # @param amount [Integer] The amount to decrement the field's value by.
311
+ # @raise Orchestrate::API::RequestError if field does not exist or
312
+ # if existing value is not a number type
313
+ # @return Orchestrate::KeyValue::OperationSet
314
+ def decrement(path, amount)
315
+ OperationSet.new(self).decrement(path, amount)
316
+ end
317
+ alias :dec :decrement
318
+
319
+ # Tests equality of a KeyValue's existing field/value pair
320
+ # @param path [#to_s] The field to check equality of.
321
+ # @param value [#to_json] The expected value of the field.
322
+ # @raise Orchestrate::API::RequestError if field does not exist
323
+ # @raise Orchestrate::API::IndexingError if equality check fails
324
+ # @return Orchestrate::KeyValue::OperationSet
325
+ def test(path, value)
326
+ OperationSet.new(self).test(path, value)
327
+ end
328
+
329
+ # An object which builds a set of operations for manipulating an Orchestrate::KeyValue
330
+ class OperationSet
331
+
332
+ # @return [KeyValue] The keyvalue this object will manipulate.
333
+ attr_reader :key_value
334
+
335
+ # @return operations to be performed.
336
+ attr_reader :operations
337
+
338
+ # Initialize a new OperationSet object
339
+ # @param key_value [Orchestrate::KeyValue] The keyvalue to manipulate.
340
+ def initialize(key_value, operations=[])
341
+ @key_value = key_value
342
+ @operations = operations
343
+ end
344
+
345
+ def update(ref=nil)
346
+ key_value.perform(:patch, operations, ref)
347
+ end
348
+
349
+ def add(path, value)
350
+ self.class.new(key_value, operations.push({op: "add", path: "#{path}", value: value}))
351
+ end
352
+
353
+ def remove(path)
354
+ self.class.new(key_value, operations.push({op: "remove", path: "#{path}"}))
355
+ end
356
+
357
+ def replace(path, value)
358
+ self.class.new(key_value, operations.push({op: "replace", path: "#{path}", value: value}))
359
+ end
360
+
361
+ def move(from_path, to_path)
362
+ self.class.new(key_value, operations.push({op: "move", from: "#{from_path}", path: "#{to_path}"}))
363
+ end
364
+
365
+ def copy(from_path, to_path)
366
+ self.class.new(key_value, operations.push({op: "copy", from: "#{from_path}", path: "#{to_path}"}))
367
+ end
368
+
369
+ def increment(path, amount)
370
+ self.class.new(key_value, operations.push({op: "inc", path: "#{path}", value: amount}))
371
+ end
372
+ alias :inc :increment
373
+
374
+ def decrement(path, amount)
375
+ self.class.new(key_value, operations.push({op: "inc", path: "#{path}", value: -amount}))
376
+ end
377
+ alias :dec :decrement
378
+
379
+ def test(path, value)
380
+ self.class.new(key_value, operations.push({op: "test", path: "#{path}", value: value}))
381
+ end
382
+ end
383
+
384
+ # @!endgroup patch-operations
385
+
229
386
  # Deletes the KeyValue item from Orchestrate using 'If-Match' with the current ref.
230
387
  # Returns false if the item failed to delete because a new ref had been created since this KeyValue was loaded.
231
388
  # @return [true, false]
@@ -1,4 +1,4 @@
1
1
  module Orchestrate
2
2
  # @return [String] The version number of the Orchestrate Gem
3
- VERSION = "0.9.2"
3
+ VERSION = "0.10.0"
4
4
  end
@@ -64,7 +64,7 @@ class KeyValueAPITest < MiniTest::Unit::TestCase
64
64
  end
65
65
 
66
66
  def test_puts_key_value_without_ref
67
- body={"foo" => "bar"}
67
+ body = {"foo" => "bar"}
68
68
  ref = "12345"
69
69
  base_location = "/v0/#{@collection}/#{@key}"
70
70
  @stubs.put(base_location) do |env|
@@ -143,6 +143,76 @@ class KeyValueAPITest < MiniTest::Unit::TestCase
143
143
  assert_equal 200, response.status
144
144
  end
145
145
 
146
+ def test_patches_key_value_without_ref
147
+ body =[ {"op" => "add", "path" => "foo", "value" => "bar"} ]
148
+ ref = "12345"
149
+ base_location = "/v0/#{@collection}/#{@key}"
150
+ @stubs.patch(base_location) do |env|
151
+ assert_authorization @basic_auth, env
152
+ assert_header 'Content-Type', 'application/json-patch+json', env
153
+ assert_equal body.to_json, env.body
154
+ headers = { "Location" => "#{base_location}/refs/#{ref}",
155
+ "Etag" => "\"#{ref}\"" }
156
+ [ 201, response_headers(headers), '' ]
157
+ end
158
+
159
+ response = @client.patch(@collection, @key, body)
160
+ assert_equal 201, response.status
161
+ assert_equal ref, response.ref
162
+ assert_equal "#{base_location}/refs/#{ref}", response.location
163
+ end
164
+
165
+ def test_patches_key_value_with_specific_ref
166
+ body = [ {"op" => "add", "path" => "foo", "value" => "bar"} ]
167
+ ref = '123456'
168
+
169
+ @stubs.patch("/v0/#{@collection}/#{@key}") do |env|
170
+ assert_authorization @basic_auth, env
171
+ assert_header 'If-Match', "\"#{ref}\"", env
172
+ assert_header 'Content-Type', 'application/json-patch+json', env
173
+ assert_equal body.to_json, env.body
174
+ [ 201, response_headers, '' ]
175
+ end
176
+
177
+ response = @client.patch(@collection, @key, body, ref)
178
+ assert_equal 201, response.status
179
+ end
180
+
181
+ def test_patch_merges_key_value_without_ref
182
+ body = {"foo" => "bar"}
183
+ ref = "12345"
184
+ base_location = "/v0/#{@collection}/#{@key}"
185
+ @stubs.patch(base_location) do |env|
186
+ assert_authorization @basic_auth, env
187
+ assert_header 'Content-Type', 'application/merge-patch+json', env
188
+ assert_equal body.to_json, env.body
189
+ headers = { "Location" => "#{base_location}/refs/#{ref}",
190
+ "Etag" => "\"#{ref}\"" }
191
+ [ 201, response_headers(headers), '' ]
192
+ end
193
+
194
+ response = @client.patch_merge(@collection, @key, body)
195
+ assert_equal 201, response.status
196
+ assert_equal ref, response.ref
197
+ assert_equal "#{base_location}/refs/#{ref}", response.location
198
+ end
199
+
200
+ def test_patch_merges_key_value_with_specific_ref
201
+ body = {"foo" => "bar"}
202
+ ref = '123456'
203
+
204
+ @stubs.patch("/v0/#{@collection}/#{@key}") do |env|
205
+ assert_authorization @basic_auth, env
206
+ assert_header 'If-Match', "\"#{ref}\"", env
207
+ assert_header 'Content-Type', 'application/merge-patch+json', env
208
+ assert_equal body.to_json, env.body
209
+ [ 201, response_headers, '' ]
210
+ end
211
+
212
+ response = @client.patch_merge(@collection, @key, body, ref)
213
+ assert_equal 201, response.status
214
+ end
215
+
146
216
  def test_delete_key_value
147
217
  @stubs.delete("/v0/#{@collection}/#{@key}") do |env|
148
218
  assert_authorization @basic_auth, env
@@ -0,0 +1,89 @@
1
+ require "test_helper"
2
+
3
+ class CollectionGeoQueriesTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @app, @stubs = make_application({parallel:true})
6
+ @items = @app[:items]
7
+
8
+ @near_query = "location:NEAR:{lat:12 lon:56 dist:1km}"
9
+ @in_query = "location:IN:{north:12 east:57 south:12 west:56}"
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_geo = lambda do |query|
14
+ case query
15
+ when "location:NEAR:{lat:12 lon:56 dist:1km}"
16
+ { "results" => 10.times.map{|i| @make_listing.call(i)}, "count" => 10, "total_count" => 10 }
17
+ when "location:IN:{north:12 east:57 south:12 west:56}"
18
+ { "results" => 12.times.map{|i| @make_listing.call(i)}, "count" => 12, "total_count" => 12 }
19
+ when "location:NEAR:{lat:12 lon:56 dist:1km}&sort=location:distance:asc"
20
+ { "results" => 10.times.map{|i| @make_listing.call(i)}, "count" => 10, "total_count" => 10 }
21
+ else
22
+ raise ArgumentError.new("unexpected query: #{env.params['query']}")
23
+ end
24
+ end
25
+
26
+ @called = false
27
+ @stubs.get("/v0/items") do |env|
28
+ @called = true
29
+ assert_equal @query, env.params['query']
30
+ body = @handle_geo.call(env.params['query'])
31
+ [ 200, response_headers, body.to_json ]
32
+ end
33
+ end
34
+
35
+ def test_near_search_without_units
36
+ @query = @near_query
37
+ results = @items.near("location", 12, 56, 1).map{|i| i }
38
+ assert_equal 10, results.length
39
+ results.each_with_index do |item, idx|
40
+ assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
41
+ assert_equal "item-#{idx}", item[1].key
42
+ assert_nil item[1].reftime
43
+ end
44
+ end
45
+
46
+ def test_near_search_with_units
47
+ @query = @near_query
48
+ results = @items.near("location", 12, 56, 1, 'km').map{|i| i }
49
+ assert_equal 10, results.length
50
+ results.each_with_index do |item, idx|
51
+ assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
52
+ assert_equal "item-#{idx}", item[1].key
53
+ assert_nil item[1].reftime
54
+ end
55
+ end
56
+
57
+ def test_near_search_with_sort
58
+ @query = @near_query
59
+ results = @items.near("location", 12, 56, 1, 'km').order(:location).map{|i| i }
60
+ assert_equal 10, results.length
61
+ results.each_with_index do |item, idx|
62
+ assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
63
+ assert_equal "item-#{idx}", item[1].key
64
+ assert_nil item[1].reftime
65
+ end
66
+ end
67
+
68
+ def test_in_bounding_box
69
+ @query = @in_query
70
+ results = @items.in("location", {north:12, east:57, south:12, west:56}).map{|i| i }
71
+ assert_equal 12, results.length
72
+ results.each_with_index do |item, idx|
73
+ assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
74
+ assert_equal "item-#{idx}", item[1].key
75
+ assert_nil item[1].reftime
76
+ end
77
+ end
78
+
79
+ def test_in_bounding_box_with_sort
80
+ @query = @in_query
81
+ results = @items.in("location", {north:12, east:57, south:12, west:56}).order(:location).map{|i| i }
82
+ assert_equal 12, results.length
83
+ results.each_with_index do |item, idx|
84
+ assert_in_delta (@total-idx/@total * 5.0), item[0], 0.005
85
+ assert_equal "item-#{idx}", item[1].key
86
+ assert_nil item[1].reftime
87
+ end
88
+ end
89
+ end
@@ -109,55 +109,55 @@ 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}
112
+ def test_set_merges_values_and_saves
113
+ set = {hello: "there", 'two' => 2}
114
114
  body = @kv.value
115
- update.each_pair {|key, value| body[key.to_s] = value }
115
+ set.each_pair {|key, value| body[key.to_s] = value }
116
116
  new_ref = make_ref
117
117
  @stubs.put("/v0/items/#{@kv.key}") do |env|
118
118
  assert_equal body, JSON.parse(env.body)
119
119
  assert_header 'If-Match', "\"#{@kv.ref}\"", env
120
120
  [201, response_headers({'Etag' => new_ref, "Location" => "/v0/items/#{@kv.key}/refs/#{new_ref}"}), '']
121
121
  end
122
- @kv.update(update)
122
+ @kv.set(set)
123
123
  assert_equal body, @kv.value
124
124
  assert_equal new_ref, @kv.ref
125
125
  end
126
126
 
127
- def test_update_merges_values_and_returns_false_on_error
128
- update = {hello: "there", 'two' => 2}
127
+ def test_set_merges_values_and_returns_false_on_error
128
+ set = {hello: "there", 'two' => 2}
129
129
  body = @kv.value
130
- update.each_pair {|key, value| body[key.to_s] = value }
130
+ set.each_pair {|key, value| body[key.to_s] = value }
131
131
  old_ref = @kv.ref
132
132
  @stubs.put("/v0/items/#{@kv.key}") do |env|
133
133
  assert_equal body, JSON.parse(env.body)
134
134
  assert_header 'If-Match', "\"#{@kv.ref}\"", env
135
135
  error_response(:service_error)
136
136
  end
137
- refute @kv.update(update)
137
+ refute @kv.set(set)
138
138
  assert_equal body, @kv.value
139
139
  assert_equal old_ref, @kv.ref
140
140
  end
141
141
 
142
- def test_update_bang_merges_values_and_save_bangs
143
- update = {hello: "there", 'two' => 2}
142
+ def test_set_bang_merges_values_and_save_bangs
143
+ set = {hello: "there", 'two' => 2}
144
144
  body = @kv.value
145
- update.each_pair {|key, value| body[key.to_s] = value }
145
+ set.each_pair {|key, value| body[key.to_s] = value }
146
146
  new_ref = make_ref
147
147
  @stubs.put("/v0/items/#{@kv.key}") do |env|
148
148
  assert_equal body, JSON.parse(env.body)
149
149
  assert_header 'If-Match', "\"#{@kv.ref}\"", env
150
150
  [201, response_headers({'Etag' => new_ref, "Location" => "/v0/items/#{@kv.key}/refs/#{new_ref}"}), '']
151
151
  end
152
- @kv.update!(update)
152
+ @kv.set!(set)
153
153
  assert_equal body, @kv.value
154
154
  assert_equal new_ref, @kv.ref
155
155
  end
156
156
 
157
- def test_update_bang_merges_values_and_save_bangs_raises_on_error
158
- update = {hello: "there", 'two' => 2}
157
+ def test_set_bang_merges_values_and_save_bangs_raises_on_error
158
+ set = {hello: "there", 'two' => 2}
159
159
  body = @kv.value
160
- update.each_pair {|key, value| body[key.to_s] = value }
160
+ set.each_pair {|key, value| body[key.to_s] = value }
161
161
  old_ref = @kv.ref
162
162
  @stubs.put("/v0/items/#{@kv.key}") do |env|
163
163
  assert_equal body, JSON.parse(env.body)
@@ -165,12 +165,397 @@ class KeyValuePersistenceTest < MiniTest::Unit::TestCase
165
165
  error_response(:service_error)
166
166
  end
167
167
  assert_raises Orchestrate::API::ServiceError do
168
- @kv.update!(update)
168
+ @kv.set!(set)
169
169
  end
170
170
  assert_equal body, @kv.value
171
171
  assert_equal old_ref, @kv.ref
172
172
  end
173
173
 
174
+ def test_merge_patch_merges_partial_value_into_existing_kv_without_ref
175
+ merge = {"foo" => "bar"}
176
+ body = @kv.value
177
+ body["foo"] = "bar"
178
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
179
+ assert_equal merge, JSON.parse(env.body)
180
+ assert_header 'Content-Type', 'application/merge-patch+json', env
181
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
182
+ end
183
+ @kv.merge(merge)
184
+ assert_equal body, @kv.value
185
+ end
186
+
187
+ def test_merge_patch_merges_partial_value_into_existing_kv_with_ref
188
+ merge = {"foo" => "bar"}
189
+ body = @kv.value
190
+ body["foo"] = "bar"
191
+ ref = @kv.ref
192
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
193
+ assert_equal merge, JSON.parse(env.body)
194
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
195
+ assert_header 'Content-Type', 'application/merge-patch+json', env
196
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
197
+ end
198
+ @kv.merge(merge, ref)
199
+ assert_equal body, @kv.value
200
+ assert_equal ref, @kv.ref
201
+ end
202
+
203
+ def test_update_add_operation_creates_new_field_and_value_without_ref
204
+ add = [{ "op" => "add", "path" => "aloha", "value" => "universe"}]
205
+ body = @kv.value
206
+ body["aloha"] = "universe"
207
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
208
+ assert_equal add, JSON.parse(env.body)
209
+ assert_header 'Content-Type', 'application/json-patch+json', env
210
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
211
+ end
212
+ @kv.add(:aloha, "universe").update()
213
+ assert_equal body, @kv.value
214
+ end
215
+
216
+ def test_update_add_operation_creates_new_field_and_value_with_ref
217
+ add = [{ "op" => "add", "path" => "aloha", "value" => "universe"}]
218
+ body = @kv.value
219
+ body["aloha"] = "universe"
220
+ ref = @kv.ref
221
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
222
+ assert_equal add, JSON.parse(env.body)
223
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
224
+ assert_header 'Content-Type', 'application/json-patch+json', env
225
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
226
+ end
227
+ @kv.add(:aloha, "universe").update(ref)
228
+ assert_equal body, @kv.value
229
+ assert_equal ref, @kv.ref
230
+ end
231
+
232
+ def test_update_remove_operation_removes_new_field_and_value_without_ref
233
+ remove = [{ "op" => "remove", "path" => "hello"}]
234
+ body = @kv.value
235
+ body.delete("hello")
236
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
237
+ assert_equal remove, JSON.parse(env.body)
238
+ assert_header 'Content-Type', 'application/json-patch+json', env
239
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
240
+ end
241
+ @kv.remove(:hello).update()
242
+ assert_equal body, @kv.value
243
+ end
244
+
245
+ def test_update_remove_operation_removes_new_field_and_value_with_ref
246
+ remove = [{ "op" => "remove", "path" => "hello"}]
247
+ body = @kv.value
248
+ body.delete("hello")
249
+ ref = @kv.ref
250
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
251
+ assert_equal remove, JSON.parse(env.body)
252
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
253
+ assert_header 'Content-Type', 'application/json-patch+json', env
254
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
255
+ end
256
+ @kv.remove(:hello).update(ref)
257
+ assert_equal body, @kv.value
258
+ assert_equal ref, @kv.ref
259
+ end
260
+
261
+ def test_update_remove_operation_raises_error_with_non_existing_field
262
+ remove = [{ "op" => "remove", "path" => "foo"}]
263
+ body = @kv.value
264
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
265
+ assert_equal remove, JSON.parse(env.body)
266
+ assert_header 'Content-Type', 'application/json-patch+json', env
267
+ error_response(:malformed_ref)
268
+ end
269
+ assert_raises Orchestrate::API::MalformedRef do
270
+ @kv.remove(:foo).update()
271
+ end
272
+ end
273
+
274
+ def test_update_replace_operation_replaces_field_with_new_value_without_ref
275
+ replace = [{ "op" => "replace", "path" => "hello", "value" => "universe"}]
276
+ body = @kv.value
277
+ body["hello"] = "universe"
278
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
279
+ assert_equal replace, JSON.parse(env.body)
280
+ assert_header 'Content-Type', 'application/json-patch+json', env
281
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
282
+ end
283
+ @kv.replace(:hello, "universe").update()
284
+ assert_equal body, @kv.value
285
+ end
286
+
287
+ def test_update_replace_operation_replaces_field_with_new_value_with_ref
288
+ replace = [{ "op" => "replace", "path" => "hello", "value" => "universe"}]
289
+ body = @kv.value
290
+ body["hello"] = "universe"
291
+ ref = @kv.ref
292
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
293
+ assert_equal replace, JSON.parse(env.body)
294
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
295
+ assert_header 'Content-Type', 'application/json-patch+json', env
296
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
297
+ end
298
+ @kv.replace(:hello, "universe").update(ref)
299
+ assert_equal body, @kv.value
300
+ assert_equal ref, @kv.ref
301
+ end
302
+
303
+ def test_update_replace_operation_raises_error_with_non_existing_field
304
+ replace = [{ "op" => "replace", "path" => "foo", "value" => "bar"}]
305
+ body = @kv.value
306
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
307
+ assert_equal replace, JSON.parse(env.body)
308
+ assert_header 'Content-Type', 'application/json-patch+json', env
309
+ error_response(:malformed_ref)
310
+ end
311
+ assert_raises Orchestrate::API::MalformedRef do
312
+ @kv.replace(:foo, "bar").update()
313
+ end
314
+ end
315
+
316
+ def test_update_move_operation_moves_field_without_ref
317
+ move = [{ "op" => "move", "from" => "hello", "path" => "foo" }]
318
+ body = @kv.value
319
+ body.delete("hello")
320
+ body["foo"] = "world"
321
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
322
+ assert_equal move, JSON.parse(env.body)
323
+ assert_header 'Content-Type', 'application/json-patch+json', env
324
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
325
+ end
326
+ @kv.move(:hello, :foo).update()
327
+ assert_equal body, @kv.value
328
+ end
329
+
330
+ def test_update_move_operation_moves_field_with_ref
331
+ move = [{ "op" => "move", "from" => "hello", "path" => "foo" }]
332
+ body = @kv.value
333
+ body.delete("hello")
334
+ body["foo"] = "world"
335
+ ref = @kv.ref
336
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
337
+ assert_equal move, JSON.parse(env.body)
338
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
339
+ assert_header 'Content-Type', 'application/json-patch+json', env
340
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
341
+ end
342
+ @kv.move(:hello, :foo).update(ref)
343
+ assert_equal body, @kv.value
344
+ assert_equal ref, @kv.ref
345
+ end
346
+
347
+ def test_update_move_operation_raises_error_with_non_existing_field
348
+ move = [{ "op" => "move", "from" => "foo", "path" => "hello" }]
349
+ body = @kv.value
350
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
351
+ assert_equal move, JSON.parse(env.body)
352
+ assert_header 'Content-Type', 'application/json-patch+json', env
353
+ error_response(:malformed_ref)
354
+ end
355
+ assert_raises Orchestrate::API::MalformedRef do
356
+ @kv.move(:foo, :hello).update()
357
+ end
358
+ end
359
+
360
+ def test_update_copy_operation_copies_field_without_ref
361
+ copy = [{ "op" => "copy", "from" => "hello", "path" => "foo" }]
362
+ body = @kv.value
363
+ body["foo"] = "world"
364
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
365
+ assert_equal copy, JSON.parse(env.body)
366
+ assert_header 'Content-Type', 'application/json-patch+json', env
367
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
368
+ end
369
+ @kv.copy(:hello, :foo).update()
370
+ assert_equal body, @kv.value
371
+ end
372
+
373
+ def test_update_copy_operation_copies_field_with_ref
374
+ copy = [{ "op" => "copy", "from" => "hello", "path" => "foo" }]
375
+ body = @kv.value
376
+ body["foo"] = "world"
377
+ ref = @kv.ref
378
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
379
+ assert_equal copy, JSON.parse(env.body)
380
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
381
+ assert_header 'Content-Type', 'application/json-patch+json', env
382
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
383
+ end
384
+ @kv.copy(:hello, :foo).update(ref)
385
+ assert_equal body, @kv.value
386
+ assert_equal ref, @kv.ref
387
+ end
388
+
389
+ def test_update_copy_operation_raises_error_with_non_existing_field
390
+ copy = [{ "op" => "copy", "from" => "foo", "path" => "hello" }]
391
+ body = @kv.value
392
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
393
+ assert_equal copy, JSON.parse(env.body)
394
+ assert_header 'Content-Type', 'application/json-patch+json', env
395
+ error_response(:malformed_ref)
396
+ end
397
+ assert_raises Orchestrate::API::MalformedRef do
398
+ @kv.copy(:foo, :hello).update()
399
+ end
400
+ end
401
+
402
+ def test_update_increment_operation_increases_field_number_value_without_ref
403
+ increment = [{ "op" => "inc", "path" => "count", "value" => 1 }]
404
+ body = @kv.value
405
+ old_count = 2
406
+ body["count"] = 3
407
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
408
+ assert_equal increment, JSON.parse(env.body)
409
+ assert_header 'Content-Type', 'application/json-patch+json', env
410
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
411
+ end
412
+ @kv.increment(:count, 1).update()
413
+ assert_equal body, @kv.value
414
+ assert_equal old_count+1, @kv.value["count"]
415
+ end
416
+
417
+ def test_update_increment_operation_increases_field_number_value_with_ref
418
+ increment = [{ "op" => "inc", "path" => "count", "value" => 1 }]
419
+ body = @kv.value
420
+ old_count = 2
421
+ body["count"] = 3
422
+ ref = @kv.ref
423
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
424
+ assert_equal increment, JSON.parse(env.body)
425
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
426
+ assert_header 'Content-Type', 'application/json-patch+json', env
427
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
428
+ end
429
+ @kv.increment(:count, 1).update(ref)
430
+ assert_equal body, @kv.value
431
+ assert_equal ref, @kv.ref
432
+ assert_equal old_count+1, @kv.value["count"]
433
+ end
434
+
435
+ def test_update_increment_operation_raises_error_with_non_number_field
436
+ increment = [{ "op" => "inc", "path" => "hello", "value" => 1 }]
437
+ body = @kv.value
438
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
439
+ assert_equal increment, JSON.parse(env.body)
440
+ assert_header 'Content-Type', 'application/json-patch+json', env
441
+ error_response(:malformed_ref)
442
+ end
443
+ assert_raises Orchestrate::API::MalformedRef do
444
+ @kv.increment(:hello, 1).update()
445
+ end
446
+ end
447
+
448
+ def test_update_increment_operation_raises_error_with_non_existing_field
449
+ increment = [{ "op" => "inc", "path" => "foo", "value" => 1 }]
450
+ body = @kv.value
451
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
452
+ assert_equal increment, JSON.parse(env.body)
453
+ assert_header 'Content-Type', 'application/json-patch+json', env
454
+ error_response(:malformed_ref)
455
+ end
456
+ assert_raises Orchestrate::API::MalformedRef do
457
+ @kv.increment(:foo, 1).update()
458
+ end
459
+ end
460
+
461
+ def test_update_decrement_operation_decreases_field_number_value_without_ref
462
+ decrement = [{ "op" => "inc", "path" => "count", "value" => -1 }]
463
+ body = @kv.value
464
+ old_count = 3
465
+ body["count"] = 2
466
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
467
+ assert_equal decrement, JSON.parse(env.body)
468
+ assert_header 'Content-Type', 'application/json-patch+json', env
469
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
470
+ end
471
+ @kv.decrement(:count, 1).update()
472
+ assert_equal body, @kv.value
473
+ assert_equal old_count-1, @kv.value["count"]
474
+ end
475
+
476
+ def test_update_decrement_operation_decreases_field_number_value_with_ref
477
+ decrement = [{ "op" => "inc", "path" => "count", "value" => -1 }]
478
+ body = @kv.value
479
+ old_count = 3
480
+ body["count"] = 2
481
+ ref = @kv.ref
482
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
483
+ assert_equal decrement, JSON.parse(env.body)
484
+ assert_header 'If-Match', "\"#{@kv.ref}\"", env
485
+ assert_header 'Content-Type', 'application/json-patch+json', env
486
+ [201, response_headers({'Etag' => ref, "Location" => "/v0/items/#{@kv.key}/refs/#{ref}"}), '']
487
+ end
488
+ @kv.decrement(:count, 1).update(ref)
489
+ assert_equal body, @kv.value
490
+ assert_equal ref, @kv.ref
491
+ assert_equal old_count-1, @kv.value["count"]
492
+ end
493
+
494
+ def test_update_decrement_operation_raises_error_with_non_number_field
495
+ decrement = [{ "op" => "inc", "path" => "hello", "value" => -1 }]
496
+ body = @kv.value
497
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
498
+ assert_equal decrement, JSON.parse(env.body)
499
+ assert_header 'Content-Type', 'application/json-patch+json', env
500
+ error_response(:malformed_ref)
501
+ end
502
+ assert_raises Orchestrate::API::MalformedRef do
503
+ @kv.decrement(:hello, 1).update()
504
+ end
505
+ end
506
+
507
+ def test_update_decrement_operation_raises_error_with_non_existing_field
508
+ decrement = [{ "op" => "inc", "path" => "foo", "value" => -1 }]
509
+ body = @kv.value
510
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
511
+ assert_equal decrement, JSON.parse(env.body)
512
+ assert_header 'Content-Type', 'application/json-patch+json', env
513
+ error_response(:malformed_ref)
514
+ end
515
+ assert_raises Orchestrate::API::MalformedRef do
516
+ @kv.decrement(:foo, 1).update()
517
+ end
518
+ end
519
+
520
+ def test_update_test_operation_checks_equality_of_field_value_success
521
+ test = [{ "op" => "test", "path" => "count", "value" => 1 }]
522
+ body = @kv.value
523
+ body["count"] = 1
524
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
525
+ assert_equal test, JSON.parse(env.body)
526
+ assert_header 'Content-Type', 'application/json-patch+json', env
527
+ [201, response_headers({'Etag' => @kv.ref, "Location" => "/v0/items/#{@kv.key}/refs/#{@kv.ref}"}), '']
528
+ end
529
+ @kv.test(:count, 1).update()
530
+ assert_equal body, @kv.value
531
+ end
532
+
533
+ def test_update_test_operation_checks_equality_of_field_value_failure
534
+ test = [{ "op" => "test", "path" => "count", "value" => 2 }]
535
+ body = @kv.value
536
+ body["count"] = 1
537
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
538
+ assert_header 'Content-Type', 'application/json-patch+json', env
539
+ error_response(:indexing_conflict)
540
+ end
541
+ assert_raises Orchestrate::API::IndexingConflict do
542
+ @kv.test(:foo, 1).update()
543
+ end
544
+ end
545
+
546
+ def test_update_test_operation_raises_error_with_non_existing_field
547
+ test = [{ "op" => "test", "path" => "foo", "value" => 1 }]
548
+ body = @kv.value
549
+ @stubs.patch("/v0/items/#{@kv.key}") do |env|
550
+ assert_equal test, JSON.parse(env.body)
551
+ assert_header 'Content-Type', 'application/json-patch+json', env
552
+ error_response(:malformed_ref)
553
+ end
554
+ assert_raises Orchestrate::API::MalformedRef do
555
+ @kv.test(:foo, 1).update()
556
+ end
557
+ end
558
+
174
559
  def test_destroy_performs_delete_if_match_and_returns_true_on_success
175
560
  @stubs.delete("/v0/items/#{@kv.key}") do |env|
176
561
  assert_header 'If-Match', "\"#{@kv.ref}\"", env
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.9.2
4
+ version: 0.10.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-11-19 00:00:00.000000000 Z
13
+ date: 2014-12-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: faraday
@@ -150,6 +150,7 @@ files:
150
150
  - test/orchestrate/application_test.rb
151
151
  - test/orchestrate/client_test.rb
152
152
  - test/orchestrate/collection_enumeration_test.rb
153
+ - test/orchestrate/collection_geo_queries_test.rb
153
154
  - test/orchestrate/collection_kv_accessors_test.rb
154
155
  - test/orchestrate/collection_searching_test.rb
155
156
  - test/orchestrate/collection_sorting_test.rb
@@ -195,6 +196,7 @@ test_files:
195
196
  - test/orchestrate/application_test.rb
196
197
  - test/orchestrate/client_test.rb
197
198
  - test/orchestrate/collection_enumeration_test.rb
199
+ - test/orchestrate/collection_geo_queries_test.rb
198
200
  - test/orchestrate/collection_kv_accessors_test.rb
199
201
  - test/orchestrate/collection_searching_test.rb
200
202
  - test/orchestrate/collection_sorting_test.rb