fmrest-spyke 0.24.0 → 0.26.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: 9e1fed1df9ac96ceef8872d171e85bf8b2d07278ae01ed9208bee2d92e71c8d5
4
+ data.tar.gz: 7eee364c2aaa75edc7af92b6faeea94995ae8985a0571f5e01f1c7621073d86b
5
5
  SHA512:
6
- metadata.gz: 37fed4c2e1598c3b7defddcdacf2d0fbae933e85d4ccb553a220e8660554f2a21a772c8e366309fe05ab28d28439812e7aff80ff077331f84a223689454daabb
7
- data.tar.gz: ae11bbc7230e79ef021a15543a4a67ef6d0e8164d1aa92615e6a9d2e95cc0e467e2fae963a346edd88089850d1d6e747f4e30bc2626f4179275f7986c9b47ac5
6
+ metadata.gz: fb7aebd7dc09384f4b5053119cc008e12fa40d61b27702dc25676dc6dcbbc90508ad4f0855552928136aeb29ccef7fde41f388e72f1fcef1bd291a66976d4fa7
7
+ data.tar.gz: bdd921d574d31967548726033228886ecbc724dfd33a92611801364a2f4bed598f30bea9c3541e95d8d1315f6d9fa8dcf0f8afcce642f0b771ffb910388c505b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.26.0
4
+
5
+ * Add support for ActiveSupport 7.1
6
+ * Drop support for ActiveSupport < 5.2
7
+
8
+ ### 0.25.0-rc1
9
+
10
+ * Add `.and` query method
11
+ * Fix crash when `.match` query method was given non-string values
12
+
3
13
  ### 0.24.0
4
14
 
5
15
  * Add `FmRest::Layout.ignore_mod_id` flag
data/README.md CHANGED
@@ -584,10 +584,11 @@ FM Data API reference: https://help.claris.com/en/data-api-guide/
584
584
 
585
585
  \* You can manually supply the URL and JSON to a `FmRest` connection.
586
586
 
587
- ## Supported Ruby versions
587
+ ## Supported Ruby and Rails 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 3.0 through 3.3.0-rc1, and Rails (ActiveSupport) 6.1 through 7.1.
591
592
 
592
593
  ## Gem development
593
594
 
@@ -16,12 +16,8 @@ module FmRest
16
16
  # to parse the portalData JSON in SpykeFormatter
17
17
  #
18
18
  # TODO: Replace this with options in PortalBuilder
19
- class_attribute :portal_options, instance_accessor: false, instance_predicate: false
20
-
21
- # class_attribute supports a :default option since ActiveSupport 5.2,
22
- # but we want to support previous versions too so we set the default
23
- # manually instead
24
- self.portal_options = {}.freeze
19
+ class_attribute :portal_options, instance_accessor: false, instance_predicate: false,
20
+ default: {}.freeze
25
21
 
26
22
  class << self; private :portal_options=; end
27
23
 
@@ -22,16 +22,18 @@ module FmRest
22
22
  # when calling ActiveModels' define_attribute_method, otherwise it
23
23
  # will define an `attribute` method which overrides the one provided
24
24
  # by Spyke
25
- self.attribute_method_matchers.shift
25
+ if respond_to? :attribute_method_patterns
26
+ # ActiveModel >= 7.1
27
+ attribute_method_patterns.shift
28
+ else
29
+ # ActiveModel < 7.1
30
+ attribute_method_matchers.shift
31
+ end
26
32
 
27
33
  # Keep track of attribute mappings so we can get the FM field names
28
34
  # for changed attributes
29
- class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
30
-
31
- # class_attribute supports a :default option since ActiveSupport 5.2,
32
- # but we want to support previous versions too so we set the default
33
- # manually instead
34
- self.mapped_attributes = ::ActiveSupport::HashWithIndifferentAccess.new.freeze
35
+ class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false,
36
+ default: ::ActiveSupport::HashWithIndifferentAccess.new.freeze
35
37
 
36
38
  class << self; private :mapped_attributes=; end
37
39
 
@@ -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.26.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-12-22 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.26.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.26.0.rc1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: spyke
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '5.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '5.2'
41
55
  description: fmrest-spyke is an ActiveRecord-like ORM client library for the FileMaker
42
56
  Data API built on top of fmrest-core and Spyke (https://github.com/balvig/spyke).
43
57
  email:
@@ -88,11 +102,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
102
  version: '0'
89
103
  required_rubygems_version: !ruby/object:Gem::Requirement
90
104
  requirements:
91
- - - ">="
105
+ - - ">"
92
106
  - !ruby/object:Gem::Version
93
- version: '0'
107
+ version: 1.3.1
94
108
  requirements: []
95
- rubygems_version: 3.3.3
109
+ rubygems_version: 3.4.6
96
110
  signing_key:
97
111
  specification_version: 4
98
112
  summary: FileMaker Data API ORM client library