orchestrate 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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