fmrest-spyke 0.13.0 → 0.15.2

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: aa1ef5a05cb00186bcd92d32bcde44a712c751a957ec5306064a9412f4fa3df4
4
- data.tar.gz: b6f3766567c79b4c92cae9e1edb0dc7b83f573a635cbffc386a7caadd8062a63
3
+ metadata.gz: a23ace303897e8ce4f81635a6b800aae8f4cba61f79bc9594ba43669ad900fd8
4
+ data.tar.gz: 117e2ec52a4fdbfec79ec846160b8bda7a5a4a43ea0059de6a1557913efe7182
5
5
  SHA512:
6
- metadata.gz: c81e6e8ea6d5493c32ef28d3910c55ca168e8a19145c846a48b2c9e02dfc2466ee38b5e130dc3bec0e46d94685139e6659cea2624d5b0f9cac15c5cdfc529c0b
7
- data.tar.gz: 8ce5116617c78a2c439c5b087e8aacfef1b187d5cfb8401b63119c19dcb4362866c42878335d28abe299be31a4589c02b67466f1675c51246000c09193230db6
6
+ metadata.gz: 9b77e8a541883d8f0744653f69443c94eaf6e8fe40637107329b579791605c20086b09d79dd62e807b1bd4e9a941e998ddcac11fcac7b864908b7bfd613f88ae
7
+ data.tar.gz: fb90f1df6a5287bebe5ac77c9e88c6548c6101bdd29cbb4f2b0c9382a0d4e1781dbd717b504bc88bdc14b07770568cdc42871a9a65c90fbfeed545485a7480ca
data/.yardopts CHANGED
@@ -1,4 +1,5 @@
1
1
  --markup markdown
2
2
  --plugin activesupport-concern
3
+ lib/**/*.rb
3
4
  -
4
5
  docs/*
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.15.2
4
+
5
+ * Fix autoloading of `FmRest::Layout`
6
+
7
+ ### 0.15.0
8
+
9
+ * Much improved querying API (see documentation on querying), adding new
10
+ `.query` capabilities, as well as two new methods: `.match` and `.or`
11
+
12
+ ### 0.14.0
13
+
14
+ * Aliased `FmRest::Spyke::Base` as `FmRest::Layout` (now preferred), and
15
+ provided a shortcut version for setting the layout name (e.g. `class Foo <
16
+ FmRest::Layout("LayoutName")`)
17
+ * Made `layout` class setting subclass-inheritable
18
+
19
+ ### 0.13.1
20
+
21
+ * Fix downloading of container field data from FMS19+
22
+
3
23
  ### 0.13.0
4
24
 
5
25
  * Split `fmrest` gem into `fmrest-core` and `fmrest-spyke`. `fmrest` becomes a
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/fmrest.svg?style=flat)](https://rubygems.org/gems/fmrest)
4
4
  ![CI](https://github.com/beezwax/fmrest-ruby/workflows/CI/badge.svg)
5
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/beezwax/fmrest-ruby)
5
6
 
6
7
  A Ruby client for
7
8
  [FileMaker 18 and 19's Data API](https://help.claris.com/en/data-api-guide)
@@ -9,7 +10,7 @@ using
9
10
  [Faraday](https://github.com/lostisland/faraday) and with optional
10
11
  ActiveRecord-ish ORM features through [Spyke](https://github.com/balvig/spyke).
11
12
 
12
- fmrest-ruby only partially implements FileMaker 18's Data API.
13
+ fmrest-ruby only partially implements FileMaker 19's Data API.
13
14
  See the [implementation completeness table](#api-implementation-completeness-table)
14
15
  to see if a feature you need is natively supported by the gem.
15
16
 
@@ -17,10 +18,10 @@ to see if a feature you need is natively supported by the gem.
17
18
 
18
19
  The `fmrest` gem is a wrapper for two other gems:
19
20
 
20
- * `fmrest-core`, which provides the core Faraday connection builder, session
21
+ * `fmrest-spyke`, providing an ActiveRecord-like ORM library built on top
22
+ of `fmrest-core` and [Spyke](https://github.com/balvig/spyke).
23
+ * `fmrest-core`, providing the core Faraday connection builder, session
21
24
  management, and other core utilities.
22
- * `fmrest-spyke`, which provides an ActiveRecord-like ORM library built on top
23
- of `fmrest-core` and Spyke.
24
25
 
25
26
  ## Installation
26
27
 
@@ -30,7 +31,7 @@ Add this to your Gemfile:
30
31
  gem 'fmrest'
31
32
  ```
32
33
 
33
- Or if you just want to use the Faraday connection without the ORM features, do:
34
+ Or if you just want to use the Faraday connection without the ORM features:
34
35
 
35
36
  ```ruby
36
37
  gem 'fmrest-core'
@@ -40,10 +41,11 @@ gem 'fmrest-core'
40
41
 
41
42
  ### ORM example
42
43
 
43
- Most people would want to use the ORM features provided by `fmrest-spyke`:
44
+ Most people would want to use the ORM features:
44
45
 
45
46
  ```ruby
46
- class Honeybee < FmRest::Spyke::Base
47
+ # A Layout model connecting to the "Honeybees Web" FileMaker layout
48
+ class Honeybee < FmRest::Layout("Honeybees Web")
47
49
  # Connection settings
48
50
  self.fmrest_config = {
49
51
  host: "…",
@@ -53,13 +55,28 @@ class Honeybee < FmRest::Spyke::Base
53
55
  }
54
56
 
55
57
  # Mapped attributes
56
- attributes name: "Bee Name", age: "Bee Age"
58
+ attributes name: "Bee Name", age: "Bee Age", created_on: "Created On"
57
59
 
58
- # Portals
59
- has_portal :flowers
60
+ # Portal associations
61
+ has_portal :tasks
60
62
 
61
- # File container
63
+ # File containers
62
64
  container :photo, field_name: "Bee Photo"
65
+
66
+ # Scopes
67
+ scope :can_legally_fly, -> { query(age: ">18") }
68
+
69
+ # Client-side validations
70
+ validates :name, presence: true
71
+
72
+ # Callbacks
73
+ before_save :set_created_on
74
+
75
+ private
76
+
77
+ def set_created_on
78
+ self.created_on = Date.today
79
+ end
63
80
  end
64
81
 
65
82
  # Find a record by id
@@ -68,7 +85,7 @@ bee = Honeybee.find(9)
68
85
  bee.name = "Hutch"
69
86
 
70
87
  # Add a new record to portal
71
- bee.flowers.build(name: "Daisy")
88
+ bee.tasks.build(urgency: "Today")
72
89
 
73
90
  bee.save
74
91
  ```
@@ -88,11 +105,11 @@ connection = FmRest::V1.build_connection(
88
105
  )
89
106
 
90
107
  # Get all records (as parsed JSON)
91
- connection.get("layouts/MyFancyLayout/records")
108
+ connection.get("layouts/FancyLayout/records")
92
109
 
93
110
  # Create new record
94
111
  connection.post do |req|
95
- req.url "layouts/MyFancyLayout/records"
112
+ req.url "layouts/FancyLayout/records"
96
113
 
97
114
  # You can just pass a hash for the JSON body
98
115
  req.body = { … }
@@ -128,9 +145,9 @@ You can also pass a `:log` option for basic request logging, see the section on
128
145
  Option | Description | Format | Default
129
146
  --------------------|--------------------------------------------|-----------------------------|--------
130
147
  `:host` | Hostname with optional port, e.g. `"example.com:9000"` | String | None
131
- `:database` | | String | None
132
- `:username` | | String | None
133
- `:password` | | String | None
148
+ `:database` | The name of the database to connect to | String | None
149
+ `:username` | A Data API-ready account | String | None
150
+ `:password` | Your password | String | None
134
151
  `:account_name` | Alias of `:username` | String | None
135
152
  `:ssl` | SSL options to be forwarded to Faraday | Faraday SSL options | None
136
153
  `:proxy` | Proxy options to be forwarded to Faraday | Faraday proxy options | None
@@ -157,8 +174,8 @@ FmRest.default_connection_settings = {
157
174
  }
158
175
  ```
159
176
 
160
- These settings will be used by default by `FmRest::Spyke::Base` models whenever
161
- you don't set `fmrest_config=` explicitly, as well as by
177
+ These settings will be used by default by `FmRest::Layout` models whenever you
178
+ don't set `fmrest_config=` explicitly, as well as by
162
179
  `FmRest::V1.build_connection` in case you're setting up your Faraday connection
163
180
  manually.
164
181
 
@@ -188,11 +205,11 @@ building REST ORM models. fmrest-ruby builds its ORM features atop Spyke,
188
205
  bundled in the `fmrest-spyke` gem (already included if you're using the
189
206
  `fmrest` gem).
190
207
 
191
- To create a model you can inherit directly from `FmRest::Spyke::Base`, which is
192
- itself a subclass of `Spyke::Base`.
208
+ To create a model you can inherit directly from `FmRest::Layout` (itself a
209
+ subclass of `Spyke::Base`).
193
210
 
194
211
  ```ruby
195
- class Honeybee < FmRest::Spyke::Base
212
+ class Honeybee < FmRest::Layout
196
213
  end
197
214
  ```
198
215
 
@@ -216,17 +233,23 @@ bee = Honeybee.find(9) # GET request
216
233
 
217
234
  It's recommended that you read Spyke's documentation for more information on
218
235
  these basic features. If you've used ActiveRecord or similar ORM libraries
219
- however you'll find it quite familiar.
236
+ you'll find it quite familiar.
220
237
 
221
- In addition, `FmRest::Spyke::Base` extends `Spyke::Base` with the following
238
+ Notice that `FmRest::Layout` is aliased as `FmRest::Spyke::Base`. Previous
239
+ versions of fmrest-ruby only provided the latter version, so if you're already
240
+ using `FmRest::Spyke::Base` there's no need to rename your classes to
241
+ `FmRest::Layout`, both will continue to work interchangeably.
242
+
243
+ In addition, `FmRest::Layout` extends `Spyke::Base` with the following
222
244
  features:
223
245
 
224
- ### Model.fmrest_config=
246
+ ### FmRest::Layout.fmrest_config=
225
247
 
226
- This allows you to set your Data API connection settings on your model:
248
+ This allows you to set Data API connection settings specific to your model
249
+ class:
227
250
 
228
251
  ```ruby
229
- class Honeybee < FmRest::Spyke::Base
252
+ class Honeybee < FmRest::Layout
230
253
  self.fmrest_config = {
231
254
  host: "…",
232
255
  database: "…",
@@ -244,9 +267,8 @@ does the initial connection setup and then inherit from it in models using that
244
267
  same connection. E.g.:
245
268
 
246
269
  ```ruby
247
- class BeeBase < FmRest::Spyke::Base
248
- self.fmrest_config = { host: "…", … }
249
- }
270
+ class BeeBase < FmRest::Layout
271
+ self.fmrest_config = { host: "…", database: "", }
250
272
  end
251
273
 
252
274
  class Honeybee < BeeBase
@@ -254,34 +276,46 @@ class Honeybee < BeeBase
254
276
  end
255
277
  ```
256
278
 
279
+ Also, if not set, your model will try to use
280
+ `FmRest.default_connection_settings` instead.
281
+
257
282
  #### Connection settings overlays
258
283
 
259
284
  There may be cases where you want to use a different set of connection settings
260
285
  depending on context. For example, if you want to use username and password
261
- provided by the user in a web application. Since `Model.fmrest_config` is set
262
- at the class level, changing the username/password for the model in one context
263
- would also change it in all other contexts, leading to security issues.
286
+ provided by the user in a web application. Since `.fmrest_config`
287
+ is set at the class level, changing the username/password for the model in one
288
+ context would also change it in all other contexts, leading to security issues.
264
289
 
265
290
  To solve this scenario, fmrest-ruby provides a way of defining thread-local and
266
- reversible connection settings overlays through `Model.fmrest_config_overlay=`.
291
+ reversible connection settings overlays through
292
+ `.fmrest_config_overlay=`.
267
293
 
268
294
  See the [main document on connection setting overlays](docs/ConfigOverlays.md)
269
295
  for details on how it works.
270
296
 
271
- ### Model.layout
297
+ ### FmRest::Layout.layout
272
298
 
273
- Use `Model.layout` to define the layout for your model.
299
+ Use `layout` to set the layout name for your model.
274
300
 
275
301
  ```ruby
276
- class Honeybee < FmRest::Spyke::Base
302
+ class Honeybee < FmRest::Layout
277
303
  layout "Honeybees Web"
278
304
  end
279
305
  ```
280
306
 
281
- Note that you only need to set this if the name of the model and the name of
282
- the layout differ, otherwise the default will just work.
307
+ Alternatively, if you're inheriting from `FmRest::Layout` directly you can set
308
+ the layout name in the class definition line:
309
+
310
+ ```ruby
311
+ class Honeybee < FmRest::Layout("Honeybees Web")
312
+ ```
313
+
314
+ Note that you only need to manually set the layout name if the name of the
315
+ class and the name of the layout differ, otherwise fmrest-ruby will just use
316
+ the name of the class.
283
317
 
284
- ### Model.request_auth_token
318
+ ### FmRest::Layout.request_auth_token
285
319
 
286
320
  Requests a Data API session token using the connection settings in
287
321
  `fmrest_config` and returns it if successful, otherwise returns `false`.
@@ -290,16 +324,16 @@ You normally don't need to use this method as fmrest-ruby will automatically
290
324
  request and store session tokens for you (provided that `:autologin` is
291
325
  `true`).
292
326
 
293
- ### Model.logout
327
+ ### FmRest::Layout.logout
294
328
 
295
- Use `Model.logout` to log out from the database session (you may call it on any
329
+ Use `.logout` to log out from the database session (you may call it on any
296
330
  model that uses the database session you want to log out from).
297
331
 
298
332
  ```ruby
299
333
  Honeybee.logout
300
334
  ```
301
335
 
302
- ### Mapped Model.attributes
336
+ ### Mapped FmRest::Layout.attributes
303
337
 
304
338
  Spyke allows you to define your model's attributes using `attributes`, however
305
339
  sometimes FileMaker's field names aren't very Ruby-ORM-friendly, especially
@@ -308,7 +342,7 @@ fmrest-ruby extends `attributes`' functionality to allow you to map
308
342
  Ruby-friendly attribute names to FileMaker field names. E.g.:
309
343
 
310
344
  ```ruby
311
- class Honeybee < FmRest::Spyke::Base
345
+ class Honeybee < FmRest::Layout
312
346
  attributes first_name: "First Name", last_name: "Last Name"
313
347
  end
314
348
  ```
@@ -327,16 +361,16 @@ bee.first_name = "Queen"
327
361
  bee.attributes # => { "First Name": "Queen", "Last Name": "Buzz" }
328
362
  ```
329
363
 
330
- ### Model.has_portal
364
+ ### FmRest::Layout.has_portal
331
365
 
332
366
  You can define portal associations on your model wth `has_portal`, as such:
333
367
 
334
368
  ```ruby
335
- class Honeybee < FmRest::Spyke::Base
369
+ class Honeybee < FmRest::Layout
336
370
  has_portal :flowers
337
371
  end
338
372
 
339
- class Flower < FmRest::Spyke::Base
373
+ class Flower < FmRest::Layout
340
374
  attributes :color, :species
341
375
  end
342
376
  ```
@@ -371,8 +405,8 @@ Guides](https://guides.rubyonrails.org/active_model_basics.html#dirty).
371
405
  Since Spyke is API-agnostic it only provides a wide-purpose `.where` method for
372
406
  passing arbitrary parameters to the REST backend. fmrest-ruby however is well
373
407
  aware of its backend API, so it extends Spkye models with a bunch of useful
374
- querying methods: `.query`, `.limit`, `.offset`, `.sort`, `.portal`, `.script`,
375
- etc.
408
+ querying methods: `.query`, `.match`, `.omit`, `.limit`, `.offset`, `.sort`,
409
+ `.portal`, `.script`, etc.
376
410
 
377
411
  See the [main document on querying](docs/Querying.md) for detailed information
378
412
  on the query API methods.
@@ -393,7 +427,7 @@ detailed information on how those work.
393
427
  You can define container fields on your model class with `container`:
394
428
 
395
429
  ```ruby
396
- class Honeybee < FmRest::Spyke::Base
430
+ class Honeybee < FmRest::Layout
397
431
  container :photo, field_name: "Beehive Photo ID"
398
432
  end
399
433
  ```
@@ -411,7 +445,7 @@ details.
411
445
 
412
446
  ### Setting global field values
413
447
 
414
- You can call `.set_globals` on any `FmRest::Spyke::Base` model to set global
448
+ You can call `.set_globals` on any `FmRest::Layout` model to set global
415
449
  field values on the database that model is configured for.
416
450
 
417
451
  See the [main document on setting global field values](docs/GlobalFields.md)
@@ -435,7 +469,7 @@ FmRest.default_connection_settings = {
435
469
  }
436
470
 
437
471
  # Or in your model
438
- class LoggyBee < FmRest::Spyke::Base
472
+ class LoggyBee < FmRest::Layout
439
473
  self.fmrest_config = {
440
474
  host: "…",
441
475
 
@@ -449,7 +483,7 @@ If you need to set up more complex logging for your models can use the
449
483
  Faraday connection, e.g.:
450
484
 
451
485
  ```ruby
452
- class LoggyBee < FmRest::Spyke::Base
486
+ class LoggyBee < FmRest::Layout
453
487
  faraday do |conn|
454
488
  conn.response :logger, MyApp.logger, bodies: true
455
489
  end
@@ -460,31 +494,31 @@ end
460
494
 
461
495
  FM Data API reference: https://fmhelp.filemaker.com/docs/18/en/dataapi/
462
496
 
463
- | FM 18 Data API feature | Supported by basic connection | Supported by FmRest::Spyke::Base |
464
- |-------------------------------------|-------------------------------|----------------------------------|
465
- | Log in using HTTP Basic Auth | Yes | Yes |
466
- | Log in using OAuth | No | No |
467
- | Log in to an external data source | No | No |
468
- | Log in using a FileMaker ID account | No | No |
469
- | Log out | Yes | Yes |
470
- | Get product information | Manual* | No |
471
- | Get database names | Manual* | No |
472
- | Get script names | Manual* | No |
473
- | Get layout names | Manual* | No |
474
- | Get layout metadata | Manual* | No |
475
- | Create a record | Manual* | Yes |
476
- | Edit a record | Manual* | Yes |
477
- | Duplicate a record | Manual* | No |
478
- | Delete a record | Manual* | Yes |
479
- | Edit portal records | Manual* | Yes |
480
- | Get a single record | Manual* | Yes |
481
- | Get a range of records | Manual* | Yes |
482
- | Get container data | Manual* | Yes |
483
- | Upload container data | Manual* | Yes |
484
- | Perform a find request | Manual* | Yes |
485
- | Set global field values | Manual* | Yes |
486
- | Run a script | Manual* | Yes |
487
- | Run a script with another request | Manual* | Yes |
497
+ | FM 18 Data API feature | Supported by basic connection | Supported by FmRest::Layout |
498
+ |-------------------------------------|-------------------------------|-----------------------------|
499
+ | Log in using HTTP Basic Auth | Yes | Yes |
500
+ | Log in using OAuth | No | No |
501
+ | Log in to an external data source | No | No |
502
+ | Log in using a FileMaker ID account | No | No |
503
+ | Log out | Yes | Yes |
504
+ | Get product information | Manual* | No |
505
+ | Get database names | Manual* | No |
506
+ | Get script names | Manual* | No |
507
+ | Get layout names | Manual* | No |
508
+ | Get layout metadata | Manual* | No |
509
+ | Create a record | Manual* | Yes |
510
+ | Edit a record | Manual* | Yes |
511
+ | Duplicate a record | Manual* | No |
512
+ | Delete a record | Manual* | Yes |
513
+ | Edit portal records | Manual* | Yes |
514
+ | Get a single record | Manual* | Yes |
515
+ | Get a range of records | Manual* | Yes |
516
+ | Get container data | Manual* | Yes |
517
+ | Upload container data | Manual* | Yes |
518
+ | Perform a find request | Manual* | Yes |
519
+ | Set global field values | Manual* | Yes |
520
+ | Run a script | Manual* | Yes |
521
+ | Run a script with another request | Manual* | Yes |
488
522
 
489
523
  \* You can manually supply the URL and JSON to a `FmRest` connection.
490
524
 
@@ -496,6 +530,7 @@ the following Ruby implementations:
496
530
  * Ruby 2.5
497
531
  * Ruby 2.6
498
532
  * Ruby 2.7
533
+ * Ruby 3.0
499
534
 
500
535
  ## Gem development
501
536
 
@@ -518,6 +553,6 @@ See [LICENSE.txt](LICENSE.txt).
518
553
 
519
554
  ## Disclaimer
520
555
 
521
- This project is not sponsored by or otherwise affiliated with FileMaker, Inc,
522
- an Apple subsidiary. FileMaker is a trademark of FileMaker, Inc., registered in
523
- the U.S. and other countries.
556
+ This project is not sponsored by or otherwise affiliated with Claris
557
+ International Inc., an Apple Inc. subsidiary. FileMaker is a trademark of
558
+ Claris International Inc., registered in the U.S. and other countries.
data/lib/fmrest/spyke.rb CHANGED
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "spyke"
5
- rescue LoadError => e
6
- e.message << " (Did you include Spyke in your Gemfile?)" unless e.message.frozen?
7
- raise e
8
- end
9
-
3
+ require "spyke"
10
4
  require "fmrest"
11
5
  require "fmrest/spyke/spyke_formatter"
12
6
  require "fmrest/spyke/model"
@@ -6,4 +6,19 @@ module FmRest
6
6
  include FmRest::Spyke::Model
7
7
  end
8
8
  end
9
+
10
+ Layout = Spyke::Base
11
+
12
+ # Shortcut for creating a Layout class and setting its FM layout name.
13
+ #
14
+ # @param layout [String] The FM layout to connect this class to
15
+ #
16
+ # @return [Class] A new subclass of `FmRest::Layout` with the FM layout
17
+ # setting already set.
18
+ #
19
+ def self.Layout(layout)
20
+ Class.new(Layout) do
21
+ self.layout layout
22
+ end
23
+ end
9
24
  end
@@ -26,10 +26,10 @@ module FmRest
26
26
 
27
27
  class_methods do
28
28
  # Methods delegated to `FmRest::Spyke::Relation`
29
- delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
30
- :portals, :includes, :with_all_portals, :without_portals,
31
- :script, :find_one, :first, :any, :find_some,
32
- :find_in_batches, :find_each, to: :all
29
+ delegate :limit, :offset, :sort, :order, :query, :match, :omit,
30
+ :portal, :portals, :includes, :with_all_portals, :without_portals,
31
+ :script, :find_one, :first, :any, :find_some, :find_in_batches,
32
+ :find_each, to: :all
33
33
 
34
34
  # Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
35
35
  # one
@@ -4,9 +4,6 @@ module FmRest
4
4
  module Spyke
5
5
  module Model
6
6
  module Serialization
7
- FM_DATE_FORMAT = "%m/%d/%Y"
8
- FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
9
-
10
7
  # Spyke override -- Return FM Data API's expected JSON format,
11
8
  # including only modified fields.
12
9
  #
@@ -79,10 +76,10 @@ module FmRest
79
76
  def serialize_values!(params)
80
77
  params.transform_values! do |value|
81
78
  case value
82
- when *datetime_classes
83
- convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
84
- when *date_classes
85
- value.strftime(FM_DATE_FORMAT)
79
+ when *FmRest::V1.datetime_classes
80
+ FmRest::V1.convert_datetime_timezone(value.to_datetime, fmrest_config.timezone).strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
81
+ when *FmRest::V1.date_classes
82
+ value.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
86
83
  else
87
84
  value
88
85
  end
@@ -90,25 +87,6 @@ module FmRest
90
87
 
91
88
  params
92
89
  end
93
-
94
- def convert_datetime_timezone(dt)
95
- case fmrest_config.timezone
96
- when :utc, "utc"
97
- dt.new_offset(0)
98
- when :local, "local"
99
- dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
100
- when nil
101
- dt
102
- end
103
- end
104
-
105
- def datetime_classes
106
- [DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
107
- end
108
-
109
- def date_classes
110
- [Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
111
- end
112
90
  end
113
91
  end
114
92
  end
@@ -6,12 +6,26 @@ module FmRest
6
6
  module URI
7
7
  extend ::ActiveSupport::Concern
8
8
 
9
+ included do
10
+ # Make the layout setting inheritable
11
+ class_attribute :_layout, instance_predicate: false
12
+
13
+ class << self
14
+ protected :_layout
15
+ end
16
+ end
17
+
9
18
  class_methods do
10
- # Accessor for FM layout (helps with building the URI)
19
+ # Accessor for FM layout (user for building request URIs).
20
+ #
21
+ # @param layout [String] The FM layout to connect this class to
22
+ #
23
+ # @return [String] The current layout if manually set, or the name of
24
+ # the class otherwise
11
25
  #
12
26
  def layout(layout = nil)
13
- @layout = layout if layout
14
- @layout ||= model_name.name
27
+ self._layout = layout.dup.to_s.freeze if layout
28
+ self._layout || model_name.name
15
29
  end
16
30
 
17
31
  # Spyke override -- Extends `uri` to default to FM Data's URI schema
@@ -5,6 +5,8 @@ module FmRest
5
5
  class Relation < ::Spyke::Relation
6
6
  SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
7
7
 
8
+ class UnknownQueryKey < ArgumentError; end
9
+
8
10
  # NOTE: We need to keep limit, offset, sort, query and portal accessors
9
11
  # separate from regular params because FM Data API uses either "limit" or
10
12
  # "_limit" (or "_offset", etc.) as param keys depending on the type of
@@ -12,7 +14,7 @@ module FmRest
12
14
 
13
15
 
14
16
  attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
15
- :included_portals, :portal_params, :script_params
17
+ :or_flag, :included_portals, :portal_params, :script_params
16
18
 
17
19
  def initialize(*_args)
18
20
  super
@@ -161,16 +163,111 @@ module FmRest
161
163
  portal(false)
162
164
  end
163
165
 
166
+ # Sets conditions for a find request. Conditions must be given in
167
+ # `{ field: condition }` format, where `condition` is normally a string
168
+ # sent raw to the Data API server, so you can use FileMaker find
169
+ # operators. You can also pass Ruby range or date/datetime objects for
170
+ # condition values, and they'll be converted to the appropriate Data API
171
+ # representation.
172
+ #
173
+ # Passing `omit: true` in a conditions set will negate all conditions in
174
+ # that set.
175
+ #
176
+ # You can modify the way conditions are added (i.e. through logical AND
177
+ # or OR) by pre-chaining `.or`. By default it adds conditions through
178
+ # logical AND.
179
+ #
180
+ # Note that because of the way the Data API works, logical AND conditions
181
+ # on a single field are not possible. Because of that, if you try to set
182
+ # two AND conditions for the same field, the previously existing one will
183
+ # be overwritten with the new condition.
184
+ #
185
+ # It is recommended that you learn how the Data API represents conditions
186
+ # in its find requests (i.e. an array of JSON objects with conditions on
187
+ # fields). This method internally uses that same representation, which
188
+ # you can view by inspecting the resulting relations. Understanding that
189
+ # representation will also make the limitations of this Ruby API clear.
190
+ #
191
+ # @example
192
+ # Person.query(name: "=Alice") # Simple query
193
+ # Person.query(age: (20..29)) # Query using a Ruby range
194
+ # Person.query(created_on: Date.today..Date.today-1)
195
+ # Person.query(name: "=Alice", age: ">20") # Query multiple fields (logical AND)
196
+ # Person.query(name: "=Alice").query(age: ">20") # Same effect as above example
197
+ # Person.query(name: "=Bob", omit: true) # Negate a query (i.e. find people not named Bob)
198
+ # Person.query(pets: { name: "=Snuggles" }) # Query portal fields
199
+ # Person.query({ name: "=Alice" }, { name: "=Bob" }) # Separate conditions through logical OR
200
+ # Person.query(name: "=Alice").or.query(name: "=Bob") # Same effect as above example
201
+ # @return [FmRest::Spyke::Relation] a new relation with the given find
202
+ # conditions applied
164
203
  def query(*params)
165
204
  with_clone do |r|
166
- r.query_params += params.flatten.map { |p| normalize_query_params(p) }
205
+ params = params.flatten.map { |p| normalize_query_params(p) }
206
+
207
+ if r.or_flag || r.query_params.empty?
208
+ r.query_params += params
209
+ r.or_flag = nil
210
+ elsif params.length > r.query_params.length
211
+ params[0, r.query_params.length].each_with_index do |p, i|
212
+ r.query_params[i].merge!(p)
213
+ end
214
+
215
+ remainder = params.length - r.query_params.length
216
+ r.query_params += params[-remainder, remainder]
217
+ else
218
+ params.each_with_index { |p, i| r.query_params[i].merge!(p) }
219
+ end
167
220
  end
168
221
  end
169
222
 
223
+ # Similar to `.query`, but sets exact string match queries (i.e.
224
+ # prefixes queries with ==) and escapes find operators in the given
225
+ # queries using `FmRest.e`.
226
+ #
227
+ # @example
228
+ # Person.query(email: "bob@example.com") # Find exact email
229
+ # @return [FmRest::Spyke::Relation] a new relation with the exact match
230
+ # conditions applied
231
+ def match(*params)
232
+ query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v)}" })
233
+ end
234
+
235
+ # Negated version of `.query`, sets conditions to omit in a find request.
236
+ #
237
+ # This is the same as passing `omit: true` to `.query`.
238
+ #
239
+ # @return [FmRest::Spyke::Relation] a new relation with the given find
240
+ # conditions applied negated
170
241
  def omit(params)
171
242
  query(params.merge(omit: true))
172
243
  end
173
244
 
245
+ # Signals that the next query conditions to be set (through `.query`,
246
+ # `.match`, etc.) should be added as a logical OR relative to previously
247
+ # set conditions (rather than the default AND).
248
+ #
249
+ # In practice this means the JSON query request will have a new
250
+ # conditions object appended, e.g.:
251
+ #
252
+ # ```
253
+ # {"query": [{"field": "condition"}, {"field": "OR-added condition"}]}
254
+ # ```
255
+ #
256
+ # You can call this method with or without parameters. If parameters are
257
+ # given they will be passed down to `.query` (and those conditions
258
+ # immediately set), otherwise it just prepares the next
259
+ # conditions-setting method to use OR.
260
+ #
261
+ # @example
262
+ # # Add conditions directly in .or call:
263
+ # Person.query(name: "=Alice").or(name: "=Bob")
264
+ # # Add exact match conditions through method chaining
265
+ # Person.match(email: "alice@example.com").or.match(email: "bob@example.com")
266
+ def or(*params)
267
+ clone = with_clone { |r| r.or_flag = true }
268
+ params.empty? ? clone : clone.query(*params)
269
+ end
270
+
174
271
  # @return [Boolean] whether a query was set on this relation
175
272
  def has_query?
176
273
  query_params.present?
@@ -328,11 +425,91 @@ module FmRest
328
425
  next
329
426
  end
330
427
 
331
- # TODO: Raise ArgumentError if an attribute given as symbol isn't defiend
332
- if k.kind_of?(Symbol) && klass.mapped_attributes.has_key?(k)
333
- normalized[klass.mapped_attributes[k].to_s] = v
428
+ # Portal fields query (nested hash), e.g. { contact: { name: "Hutch" } }
429
+ if v.kind_of?(Hash)
430
+ if k.kind_of?(Symbol)
431
+ portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == k.to_s }
432
+
433
+ if portal_key
434
+ portal_model = klass.associations[k].klass
435
+
436
+ portal_normalized = v.each_with_object({}) do |(pk, pv), h|
437
+ normalize_single_query_param_for_model(portal_model, pk, pv, h)
438
+ end
439
+
440
+ normalized.merge!(portal_normalized.transform_keys { |pf| "#{portal_key}::#{pf}" })
441
+ else
442
+ raise UnknownQueryKey, "No portal matches the query key `:#{k}` on #{klass.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
443
+ end
444
+ else
445
+ normalized.merge!(v.transform_keys { |pf| "#{k}::#{pf}" })
446
+ end
447
+
448
+ next
449
+ end
450
+
451
+ # Attribute query (scalar values), e.g. { name: "Hutch" }
452
+ normalize_single_query_param_for_model(klass, k, v, normalized)
453
+ end
454
+ end
455
+
456
+ def normalize_single_query_param_for_model(model, k, v, hash)
457
+ if k.kind_of?(Symbol)
458
+ if model.mapped_attributes.has_key?(k)
459
+ hash[model.mapped_attributes[k].to_s] = format_query_condition(v)
460
+ else
461
+ raise UnknownQueryKey, "No attribute matches the query key `:#{k}` on #{model.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
462
+ end
463
+ else
464
+ hash[k.to_s] = format_query_condition(v)
465
+ end
466
+ end
467
+
468
+ # Transforms various Ruby data types to FileMaker search condition
469
+ # strings
470
+ #
471
+ def format_query_condition(condition)
472
+ case condition
473
+ when nil
474
+ "=" # Search for empty field
475
+ when Range
476
+ format_range_condition(condition)
477
+ when *FmRest::V1.datetime_classes
478
+ FmRest::V1.convert_datetime_timezone(condition.to_datetime, klass.fmrest_config.timezone)
479
+ .strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
480
+ when *FmRest::V1.date_classes
481
+ condition.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
482
+ else
483
+ condition
484
+ end
485
+ end
486
+
487
+ def format_range_condition(range)
488
+ if range.first.kind_of?(Numeric)
489
+ if range.first == Float::INFINITY || range.end == -Float::INFINITY
490
+ raise ArgumentError, "Can't search for a range that begins at +Infinity or ends at -Infinity"
491
+ elsif range.first == -Float::INFINITY
492
+ if range.end == Float::INFINITY || range.end.nil?
493
+ "*" # Search for non-empty field
494
+ else
495
+ range.exclude_end? ? "<#{range.end}" : "<=#{range.end}"
496
+ end
497
+ elsif range.end == Float::INFINITY || range.end.nil?
498
+ ">=#{range.first}"
499
+ elsif range.exclude_end? && range.last.respond_to?(:pred)
500
+ "#{range.first}..#{range.last.pred}"
334
501
  else
335
- normalized[k.to_s] = v
502
+ "#{range.first}..#{range.last}"
503
+ end
504
+ else
505
+ "#{format_query_condition(range.first)}..#{format_query_condition(range.last)}"
506
+ end
507
+ end
508
+
509
+ def transform_query_values(*params, &block)
510
+ params.flatten.map do |p|
511
+ p.transform_values do |v|
512
+ v.kind_of?(Hash) ? v.transform_values(&block) : yield(v)
336
513
  end
337
514
  end
338
515
  end
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.13.0
4
+ version: 0.15.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-11 00:00:00.000000000 Z
11
+ date: 2021-04-06 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.13.0
19
+ version: 0.15.2
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.13.0
26
+ version: 0.15.2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: spyke
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  requirements: []
92
- rubygems_version: 3.2.3
92
+ rubygems_version: 3.0.6
93
93
  signing_key:
94
94
  specification_version: 4
95
95
  summary: FileMaker Data API ORM client library