fmrest-spyke 0.24.0 → 0.25.0.rc1

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
  SHA256:
3
- metadata.gz: a5e06c891b093db66e7dc2c47d78dd946c2f0d11777595261df7f4292925dad1
4
- data.tar.gz: 2661cfb08f3960786368aad7b7f5b4192dc127741be54a9391c5f870d30eee4f
3
+ metadata.gz: fe351af8101b6ca643caf8c389106bae00d5fe72aacf9fa8dba586aef31a5797
4
+ data.tar.gz: e366111a110101ce574522375ef057ca448e5baf0f5ddc3e952d956534df3ec4
5
5
  SHA512:
6
- metadata.gz: 37fed4c2e1598c3b7defddcdacf2d0fbae933e85d4ccb553a220e8660554f2a21a772c8e366309fe05ab28d28439812e7aff80ff077331f84a223689454daabb
7
- data.tar.gz: ae11bbc7230e79ef021a15543a4a67ef6d0e8164d1aa92615e6a9d2e95cc0e467e2fae963a346edd88089850d1d6e747f4e30bc2626f4179275f7986c9b47ac5
6
+ metadata.gz: f3e875be7b1d271a8ab1a56375d73654f47bf27cee4bbb7f45904c78e5a492874317e7b023590a3c255ad089ab02485298ac1511731335a0df40516767e0a8fa
7
+ data.tar.gz: 122579f2b002c0c1d10e4f5cb935afaace304f43c12051f100200111fdf9982a1559bdd1e2504c50e509d550c99402d9026086ac1ca48f8172b3fc37562042e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.25.0
4
+
5
+ * Add `.and` query method
6
+ * Fix crash when `.match` query method was given non-string values
7
+
3
8
  ### 0.24.0
4
9
 
5
10
  * Add `FmRest::Layout.ignore_mod_id` flag
data/README.md CHANGED
@@ -586,8 +586,9 @@ FM Data API reference: https://help.claris.com/en/data-api-guide/
586
586
 
587
587
  ## Supported Ruby versions
588
588
 
589
- fmrest-ruby is [tested against](https://github.com/beezwax/fmrest-ruby/actions?query=workflow%3ACI)
590
- Ruby 2.6 through 3.1.
589
+ The latest fmrest-ruby is
590
+ [tested against](https://github.com/beezwax/fmrest-ruby/actions?query=workflow%3ACI)
591
+ Ruby 2.7 through 3.2.
591
592
 
592
593
  ## Gem development
593
594
 
@@ -25,7 +25,7 @@ module FmRest
25
25
  delegate :limit, :offset, :sort, :order, :query, :match, :omit,
26
26
  :portal, :portals, :includes, :with_all_portals, :without_portals,
27
27
  :script, :find_one, :first, :any, :find_some, :find_in_batches,
28
- :find_each, to: :all
28
+ :find_each, :and, :or, to: :all
29
29
 
30
30
  # Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
31
31
  # one
@@ -34,9 +34,9 @@ module FmRest
34
34
  current_scope || Relation.new(self, uri: uri)
35
35
  end
36
36
 
37
- # Spyke override -- allows properly setting limit, offset and other
38
- # options, as well as using the appropriate HTTP method/URL depending
39
- # on whether there's a query present in the current scope.
37
+ # Spyke override -- properly sets limit, offset and other options, as
38
+ # well as using the appropriate HTTP method/URL depending on whether
39
+ # there's a query present in the current scope.
40
40
  #
41
41
  # @option options [Boolean] :raise_on_no_matching_records whether to
42
42
  # raise `APIError::NoMatchingRecordsError` when no records match (FM
@@ -5,6 +5,18 @@ module FmRest
5
5
  class Relation < ::Spyke::Relation
6
6
  SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
7
7
 
8
+ # This needs to use four-digit numbers in order to work with Date fields
9
+ # also, otherwise FileMaker will complain about date formatting
10
+ ZERO_RESULTS_QUERY = '1001..1000'
11
+
12
+ UNSATISFIABLE_QUERY_VALUE =
13
+ Object.new.tap do |u|
14
+ def u.inspect; 'Unsatisfiable'; end
15
+ def u.to_s; ZERO_RESULTS_QUERY; end
16
+ end.freeze
17
+
18
+ NORMALIZED_OMIT_KEY = 'omit'
19
+
8
20
  class UnknownQueryKey < ArgumentError; end
9
21
 
10
22
  # NOTE: We need to keep limit, offset, sort, query and portal accessors
@@ -14,7 +26,8 @@ module FmRest
14
26
 
15
27
 
16
28
  attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
17
- :or_flag, :included_portals, :portal_params, :script_params
29
+ :chain_flag, :included_portals, :portal_params,
30
+ :script_params
18
31
 
19
32
  def initialize(*_args)
20
33
  super
@@ -204,9 +217,12 @@ module FmRest
204
217
  with_clone do |r|
205
218
  params = params.flatten.map { |p| normalize_query_params(p) }
206
219
 
207
- if r.or_flag || r.query_params.empty?
220
+ if r.chain_flag == :or || r.query_params.empty?
208
221
  r.query_params += params
209
- r.or_flag = nil
222
+ r.chain_flag = nil
223
+ elsif r.chain_flag == :and
224
+ r.cartesian_product_query_params(params)
225
+ r.chain_flag = nil
210
226
  elsif params.length > r.query_params.length
211
227
  params[0, r.query_params.length].each_with_index do |p, i|
212
228
  r.query_params[i].merge!(p)
@@ -229,22 +245,22 @@ module FmRest
229
245
  # @return [FmRest::Spyke::Relation] a new relation with the exact match
230
246
  # conditions applied
231
247
  def match(*params)
232
- query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v)}" })
248
+ query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v.to_s)}" })
233
249
  end
234
250
 
235
- # Negated version of `.query`, sets conditions to omit in a find request.
251
+ # Adds a new set of conditions to omit in a find request.
236
252
  #
237
- # This is the same as passing `omit: true` to `.query`.
253
+ # This is the same as passing `omit: true` to `.or`.
238
254
  #
239
255
  # @return [FmRest::Spyke::Relation] a new relation with the given find
240
256
  # conditions applied negated
241
257
  def omit(params)
242
- query(params.merge(omit: true))
258
+ self.or(params.merge(omit: true))
243
259
  end
244
260
 
245
261
  # Signals that the next query conditions to be set (through `.query`,
246
262
  # `.match`, etc.) should be added as a logical OR relative to previously
247
- # set conditions (rather than the default AND).
263
+ # set conditions.
248
264
  #
249
265
  # In practice this means the JSON query request will have a new
250
266
  # conditions object appended, e.g.:
@@ -256,15 +272,91 @@ module FmRest
256
272
  # You can call this method with or without parameters. If parameters are
257
273
  # given they will be passed down to `.query` (and those conditions
258
274
  # immediately set), otherwise it just prepares the next
259
- # conditions-setting method to use OR.
275
+ # conditions-setting method (e.g. `match`) to use OR.
260
276
  #
261
277
  # @example
262
- # # Add conditions directly in .or call:
278
+ # # Add conditions directly on .or call:
263
279
  # Person.query(name: "=Alice").or(name: "=Bob")
264
280
  # # Add exact match conditions through method chaining
265
281
  # Person.match(email: "alice@example.com").or.match(email: "bob@example.com")
266
282
  def or(*params)
267
- clone = with_clone { |r| r.or_flag = true }
283
+ clone = with_clone { |r| r.chain_flag = :or }
284
+ params.empty? ? clone : clone.query(*params)
285
+ end
286
+
287
+ # Signals that the next query conditions to be set (through `.query`,
288
+ # `.match`, etc.) should be added as a logical AND relative to previously
289
+ # set conditions.
290
+ #
291
+ # In practice this means the given conditions will be applied through
292
+ # cartesian product onto the previously defined conditions objects in the
293
+ # JSON query request.
294
+ #
295
+ # For example, if you had these conditions:
296
+ #
297
+ # ```
298
+ # [{name: "Alice"}, {name: "Bob"}]
299
+ # ```
300
+ #
301
+ # After calling `.and(age: 20)`, the conditions would look like:
302
+ #
303
+ # ```
304
+ # [{name: "Alice", age: 20}, {name: "Bob", age: 20}]
305
+ # ```
306
+ #
307
+ # Or in pseudocode logical representation:
308
+ #
309
+ # ```
310
+ # (name = "Alice" OR name = "Bob") AND age = 20
311
+ # ```
312
+ #
313
+ # You can also pass multiple condition hashes to `.and`, in which case
314
+ # it will treat them as OR-separated, e.g.:
315
+ #
316
+ # ```
317
+ # .query({ name: "Alice" }, { name: "Bob" }).and({ age: 20 }, { age: 30 })
318
+ # ```
319
+ #
320
+ # Would result in the following conditions:
321
+ #
322
+ # ```
323
+ # [
324
+ # {name: "Alice", age: 20 },
325
+ # {name: "Alice", age: 30 },
326
+ # {name: "Bob", age: 20 },
327
+ # {name: "Bob", age: 30 }
328
+ # ]
329
+ # ```
330
+ #
331
+ # In pseudocode:
332
+ #
333
+ # ```
334
+ # (name = "Alice" OR name = "Bob") AND (age = 20 OR age = 30)
335
+ # ```
336
+ #
337
+ # You can call this method with or without parameters. If parameters are
338
+ # given they will be passed down to `.query` (and those conditions
339
+ # immediately set), otherwise it just prepares the next
340
+ # conditions-setting method (e.g. `match`) to use AND.
341
+ #
342
+ # Note that if you use this method on fields that already had conditions
343
+ # set you may end up with an unsatisfiable condition (e.g. name matches
344
+ # 'Bob' AND 'Alice' simultaneously). In that case fmrest-ruby will
345
+ # replace your given values with an expression that's guaranteed to
346
+ # return zero results, as that is the logically expected result.
347
+ #
348
+ # @example
349
+ # # Add conditions directly on .and call:
350
+ # Person.query(name: "=Alice").and(city: "=Wonderland")
351
+ # # Add exact match conditions through method chaining:
352
+ # Person.match(name: "Alice").and.match(city: "Wonderland")
353
+ # # With multiple condition hashes:
354
+ # Person.query(name: "=Alice").and({ city: "=Wonderland" }, { city: "=London" })
355
+ # # With conflicting criteria:
356
+ # Person.match(name: "Alice").and.match(name: "Bob")
357
+ # # => JSON: { "name": "1001..1000" } -> forced empty result set
358
+ def and(*params)
359
+ clone = with_clone { |r| r.chain_flag = :and }
268
360
  params.empty? ? clone : clone.query(*params)
269
361
  end
270
362
 
@@ -397,8 +489,33 @@ module FmRest
397
489
  end
398
490
  end
399
491
 
492
+ def cartesian_product_query_params(params)
493
+ if (query_params + params).any? { |p| p.key?(NORMALIZED_OMIT_KEY) }
494
+ raise ArgumentError, "Cannot use `and' with queries containing `omit'"
495
+ end
496
+
497
+ self.query_params =
498
+ query_params
499
+ .product(params)
500
+ .map { |a, b| a.merge(b) { |k, v1, v2| v1 == v2 ? v1 : unsatisfiable(k, v1, v2) } }
501
+ end
502
+
400
503
  private
401
504
 
505
+ def unsatisfiable(field, a, b)
506
+ unless a == UNSATISFIABLE_QUERY_VALUE || b == UNSATISFIABLE_QUERY_VALUE
507
+ # TODO: Add a setting to make this an exception instead of a warning?
508
+ warn(
509
+ "An FmRest query using `and' required that `#{field}' match " \
510
+ "'#{a}' and '#{b}' at the same time which can't be satisified. " \
511
+ "This will appear in the find request as '#{UNSATISFIABLE_QUERY_VALUE}' " \
512
+ "and may result in an empty resultset."
513
+ )
514
+ end
515
+
516
+ UNSATISFIABLE_QUERY_VALUE
517
+ end
518
+
402
519
  def normalize_sort_param(param)
403
520
  if param.kind_of?(Symbol) || param.kind_of?(String)
404
521
  _, attr, descend = param.to_s.match(SORT_PARAM_MATCHER).to_a
@@ -432,10 +549,10 @@ module FmRest
432
549
 
433
550
  def normalize_query_params(params)
434
551
  params.each_with_object({}) do |(k, v), normalized|
435
- if k == :omit || k == "omit"
552
+ if k == :omit || k == NORMALIZED_OMIT_KEY
436
553
  # FM Data API wants omit values as strings, e.g. "true" or "false"
437
554
  # rather than true/false
438
- normalized["omit"] = v.to_s
555
+ normalized[NORMALIZED_OMIT_KEY] = v.to_s
439
556
  next
440
557
  end
441
558
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fmrest-spyke
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.25.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-14 00:00:00.000000000 Z
11
+ date: 2023-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fmrest-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.24.0
19
+ version: 0.25.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.24.0
26
+ version: 0.25.0.rc1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: spyke
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -88,9 +88,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
88
  version: '0'
89
89
  required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  requirements:
91
- - - ">="
91
+ - - ">"
92
92
  - !ruby/object:Gem::Version
93
- version: '0'
93
+ version: 1.3.1
94
94
  requirements: []
95
95
  rubygems_version: 3.3.3
96
96
  signing_key: