fmrest-spyke 0.13.1 → 0.16.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
  SHA256:
3
- metadata.gz: be7fffcbab34bb4d18fbca7c7279562d5ae9fea61ee7a3bea30885062ef50c73
4
- data.tar.gz: e1148f44c5d730ca4406eb5aae58494b33f3562102371a3a0ce7073f5f0558e2
3
+ metadata.gz: 1a91619fdca8717119b8b299648d3388948551e653bececb0a90a422850be0a2
4
+ data.tar.gz: 9e666b8f8162522b1269a3b4d18cda8724ad7439d0398672c02d228149cb636d
5
5
  SHA512:
6
- metadata.gz: 5c73415699cfef77051e1365d89c178fe05436ffef4134b88972ed93b921f2b32131007f9c3b4647ba1fcce4d6ab6f24ba924e1ae043c84cfd09c368b84b4f32
7
- data.tar.gz: fac0d4d6e9f3d8397187f1c9dba29cfe62370069086aadc0760778e2d4b0fb32d29b318d534d9548896c0fbe364a52c9e27585da60b327aa465ab19cdf328a07
6
+ metadata.gz: 6fee0ccdc7bb75ae47f620c0cbf3da137806394225a565f5227d9a63e4ea4612b78200997dfc84594ba4eb2db84a156a28d3b05109d9dfb95f6851434bd94f6b
7
+ data.tar.gz: bab901d8024b1331358325f149ebc297c206e549aba81b73187748b2f168c7eecac2409170722e7a062901317084348c4ac3811bddffa07613bf944aa0f61f00
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,26 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.16.0
4
+
5
+ * Add `FmRest.logger=`
6
+ * Handle serialization of `nil`, `true` and `false` values
7
+
8
+ ### 0.15.2
9
+
10
+ * Fix autoloading of `FmRest::Layout`
11
+
12
+ ### 0.15.0
13
+
14
+ * Much improved querying API (see documentation on querying), adding new
15
+ `.query` capabilities, as well as two new methods: `.match` and `.or`
16
+
17
+ ### 0.14.0
18
+
19
+ * Aliased `FmRest::Spyke::Base` as `FmRest::Layout` (now preferred), and
20
+ provided a shortcut version for setting the layout name (e.g. `class Foo <
21
+ FmRest::Layout("LayoutName")`)
22
+ * Made `layout` class setting subclass-inheritable
23
+
3
24
  ### 0.13.1
4
25
 
5
26
  * Fix downloading of container field data from FMS19+
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,13 +145,14 @@ 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
137
154
  `:log` | Log JSON responses to STDOUT | Boolean | `false`
155
+ `:log_level` | Which log level to log into | Values accepted by `Logger#level=` | `:debug`
138
156
  `:coerce_dates` | See section on [date fields](#date-fields-and-timezones) | Boolean \| `:hybrid` \| `:full` | `false`
139
157
  `:date_format` | Date parsing format | String (FM date format) | `"MM/dd/yyyy"`
140
158
  `:timestamp_format` | Timestmap parsing format | String (FM date format) | `"MM/dd/yyyy HH:mm:ss"`
@@ -157,8 +175,8 @@ FmRest.default_connection_settings = {
157
175
  }
158
176
  ```
159
177
 
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
178
+ These settings will be used by default by `FmRest::Layout` models whenever you
179
+ don't set `fmrest_config=` explicitly, as well as by
162
180
  `FmRest::V1.build_connection` in case you're setting up your Faraday connection
163
181
  manually.
164
182
 
@@ -188,11 +206,11 @@ building REST ORM models. fmrest-ruby builds its ORM features atop Spyke,
188
206
  bundled in the `fmrest-spyke` gem (already included if you're using the
189
207
  `fmrest` gem).
190
208
 
191
- To create a model you can inherit directly from `FmRest::Spyke::Base`, which is
192
- itself a subclass of `Spyke::Base`.
209
+ To create a model you can inherit directly from `FmRest::Layout` (itself a
210
+ subclass of `Spyke::Base`).
193
211
 
194
212
  ```ruby
195
- class Honeybee < FmRest::Spyke::Base
213
+ class Honeybee < FmRest::Layout
196
214
  end
197
215
  ```
198
216
 
@@ -216,17 +234,23 @@ bee = Honeybee.find(9) # GET request
216
234
 
217
235
  It's recommended that you read Spyke's documentation for more information on
218
236
  these basic features. If you've used ActiveRecord or similar ORM libraries
219
- however you'll find it quite familiar.
237
+ you'll find it quite familiar.
238
+
239
+ Notice that `FmRest::Layout` is aliased as `FmRest::Spyke::Base`. Previous
240
+ versions of fmrest-ruby only provided the latter version, so if you're already
241
+ using `FmRest::Spyke::Base` there's no need to rename your classes to
242
+ `FmRest::Layout`, both will continue to work interchangeably.
220
243
 
221
- In addition, `FmRest::Spyke::Base` extends `Spyke::Base` with the following
244
+ In addition, `FmRest::Layout` extends `Spyke::Base` with the following
222
245
  features:
223
246
 
224
- ### Model.fmrest_config=
247
+ ### FmRest::Layout.fmrest_config=
225
248
 
226
- This allows you to set your Data API connection settings on your model:
249
+ This allows you to set Data API connection settings specific to your model
250
+ class:
227
251
 
228
252
  ```ruby
229
- class Honeybee < FmRest::Spyke::Base
253
+ class Honeybee < FmRest::Layout
230
254
  self.fmrest_config = {
231
255
  host: "…",
232
256
  database: "…",
@@ -244,9 +268,8 @@ does the initial connection setup and then inherit from it in models using that
244
268
  same connection. E.g.:
245
269
 
246
270
  ```ruby
247
- class BeeBase < FmRest::Spyke::Base
248
- self.fmrest_config = { host: "…", … }
249
- }
271
+ class BeeBase < FmRest::Layout
272
+ self.fmrest_config = { host: "…", database: "", }
250
273
  end
251
274
 
252
275
  class Honeybee < BeeBase
@@ -254,34 +277,46 @@ class Honeybee < BeeBase
254
277
  end
255
278
  ```
256
279
 
280
+ Also, if not set, your model will try to use
281
+ `FmRest.default_connection_settings` instead.
282
+
257
283
  #### Connection settings overlays
258
284
 
259
285
  There may be cases where you want to use a different set of connection settings
260
286
  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.
287
+ provided by the user in a web application. Since `.fmrest_config`
288
+ is set at the class level, changing the username/password for the model in one
289
+ context would also change it in all other contexts, leading to security issues.
264
290
 
265
291
  To solve this scenario, fmrest-ruby provides a way of defining thread-local and
266
- reversible connection settings overlays through `Model.fmrest_config_overlay=`.
292
+ reversible connection settings overlays through
293
+ `.fmrest_config_overlay=`.
267
294
 
268
295
  See the [main document on connection setting overlays](docs/ConfigOverlays.md)
269
296
  for details on how it works.
270
297
 
271
- ### Model.layout
298
+ ### FmRest::Layout.layout
272
299
 
273
- Use `Model.layout` to define the layout for your model.
300
+ Use `layout` to set the layout name for your model.
274
301
 
275
302
  ```ruby
276
- class Honeybee < FmRest::Spyke::Base
303
+ class Honeybee < FmRest::Layout
277
304
  layout "Honeybees Web"
278
305
  end
279
306
  ```
280
307
 
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.
308
+ Alternatively, if you're inheriting from `FmRest::Layout` directly you can set
309
+ the layout name in the class definition line:
283
310
 
284
- ### Model.request_auth_token
311
+ ```ruby
312
+ class Honeybee < FmRest::Layout("Honeybees Web")
313
+ ```
314
+
315
+ Note that you only need to manually set the layout name if the name of the
316
+ class and the name of the layout differ, otherwise fmrest-ruby will just use
317
+ the name of the class.
318
+
319
+ ### FmRest::Layout.request_auth_token
285
320
 
286
321
  Requests a Data API session token using the connection settings in
287
322
  `fmrest_config` and returns it if successful, otherwise returns `false`.
@@ -290,16 +325,16 @@ You normally don't need to use this method as fmrest-ruby will automatically
290
325
  request and store session tokens for you (provided that `:autologin` is
291
326
  `true`).
292
327
 
293
- ### Model.logout
328
+ ### FmRest::Layout.logout
294
329
 
295
- Use `Model.logout` to log out from the database session (you may call it on any
330
+ Use `.logout` to log out from the database session (you may call it on any
296
331
  model that uses the database session you want to log out from).
297
332
 
298
333
  ```ruby
299
334
  Honeybee.logout
300
335
  ```
301
336
 
302
- ### Mapped Model.attributes
337
+ ### Mapped FmRest::Layout.attributes
303
338
 
304
339
  Spyke allows you to define your model's attributes using `attributes`, however
305
340
  sometimes FileMaker's field names aren't very Ruby-ORM-friendly, especially
@@ -308,7 +343,7 @@ fmrest-ruby extends `attributes`' functionality to allow you to map
308
343
  Ruby-friendly attribute names to FileMaker field names. E.g.:
309
344
 
310
345
  ```ruby
311
- class Honeybee < FmRest::Spyke::Base
346
+ class Honeybee < FmRest::Layout
312
347
  attributes first_name: "First Name", last_name: "Last Name"
313
348
  end
314
349
  ```
@@ -327,16 +362,16 @@ bee.first_name = "Queen"
327
362
  bee.attributes # => { "First Name": "Queen", "Last Name": "Buzz" }
328
363
  ```
329
364
 
330
- ### Model.has_portal
365
+ ### FmRest::Layout.has_portal
331
366
 
332
367
  You can define portal associations on your model wth `has_portal`, as such:
333
368
 
334
369
  ```ruby
335
- class Honeybee < FmRest::Spyke::Base
370
+ class Honeybee < FmRest::Layout
336
371
  has_portal :flowers
337
372
  end
338
373
 
339
- class Flower < FmRest::Spyke::Base
374
+ class Flower < FmRest::Layout
340
375
  attributes :color, :species
341
376
  end
342
377
  ```
@@ -371,8 +406,8 @@ Guides](https://guides.rubyonrails.org/active_model_basics.html#dirty).
371
406
  Since Spyke is API-agnostic it only provides a wide-purpose `.where` method for
372
407
  passing arbitrary parameters to the REST backend. fmrest-ruby however is well
373
408
  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.
409
+ querying methods: `.query`, `.match`, `.omit`, `.limit`, `.offset`, `.sort`,
410
+ `.portal`, `.script`, etc.
376
411
 
377
412
  See the [main document on querying](docs/Querying.md) for detailed information
378
413
  on the query API methods.
@@ -393,7 +428,7 @@ detailed information on how those work.
393
428
  You can define container fields on your model class with `container`:
394
429
 
395
430
  ```ruby
396
- class Honeybee < FmRest::Spyke::Base
431
+ class Honeybee < FmRest::Layout
397
432
  container :photo, field_name: "Beehive Photo ID"
398
433
  end
399
434
  ```
@@ -411,7 +446,7 @@ details.
411
446
 
412
447
  ### Setting global field values
413
448
 
414
- You can call `.set_globals` on any `FmRest::Spyke::Base` model to set global
449
+ You can call `.set_globals` on any `FmRest::Layout` model to set global
415
450
  field values on the database that model is configured for.
416
451
 
417
452
  See the [main document on setting global field values](docs/GlobalFields.md)
@@ -423,7 +458,7 @@ If using `fmrest-spyke` with Rails then pretty log output will be set up for
423
458
  you automatically by Spyke (see [their
424
459
  README](https://github.com/balvig/spyke#log-output)).
425
460
 
426
- You can also enable simple Faraday STDOUT logging of raw requests (useful for
461
+ You can also enable simple Faraday logging of raw requests (useful for
427
462
  debugging) by passing `log: true` in the options hash for either
428
463
  `FmRest.default_connection_settings=` or your models' `fmrest_config=`, e.g.:
429
464
 
@@ -435,7 +470,7 @@ FmRest.default_connection_settings = {
435
470
  }
436
471
 
437
472
  # Or in your model
438
- class LoggyBee < FmRest::Spyke::Base
473
+ class LoggyBee < FmRest::Layout
439
474
  self.fmrest_config = {
440
475
  host: "…",
441
476
 
@@ -444,12 +479,22 @@ class LoggyBee < FmRest::Spyke::Base
444
479
  end
445
480
  ```
446
481
 
447
- If you need to set up more complex logging for your models can use the
482
+ You can also pass `log_level` to connection settings to change the severity of
483
+ log output (defaults to `:debug`).
484
+
485
+ By default fmrest-ruby logs to STDOUT or to Rails' logger object if available.
486
+ You can change this by providing your own logger object to `FmRest.logger=`:
487
+
488
+ ```ruby
489
+ FmRest.logger = Logger.new("fmrest.log")
490
+ ```
491
+
492
+ If you need to set up more complex logging for your models you can use the
448
493
  `faraday` block inside your class to inject your own logger middleware into the
449
494
  Faraday connection, e.g.:
450
495
 
451
496
  ```ruby
452
- class LoggyBee < FmRest::Spyke::Base
497
+ class LoggyBee < FmRest::Layout
453
498
  faraday do |conn|
454
499
  conn.response :logger, MyApp.logger, bodies: true
455
500
  end
@@ -460,31 +505,31 @@ end
460
505
 
461
506
  FM Data API reference: https://fmhelp.filemaker.com/docs/18/en/dataapi/
462
507
 
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 |
508
+ | FM 18 Data API feature | Supported by basic connection | Supported by FmRest::Layout |
509
+ |-------------------------------------|-------------------------------|-----------------------------|
510
+ | Log in using HTTP Basic Auth | Yes | Yes |
511
+ | Log in using OAuth | No | No |
512
+ | Log in to an external data source | No | No |
513
+ | Log in using a FileMaker ID account | No | No |
514
+ | Log out | Yes | Yes |
515
+ | Get product information | Manual* | No |
516
+ | Get database names | Manual* | No |
517
+ | Get script names | Manual* | No |
518
+ | Get layout names | Manual* | No |
519
+ | Get layout metadata | Manual* | No |
520
+ | Create a record | Manual* | Yes |
521
+ | Edit a record | Manual* | Yes |
522
+ | Duplicate a record | Manual* | No |
523
+ | Delete a record | Manual* | Yes |
524
+ | Edit portal records | Manual* | Yes |
525
+ | Get a single record | Manual* | Yes |
526
+ | Get a range of records | Manual* | Yes |
527
+ | Get container data | Manual* | Yes |
528
+ | Upload container data | Manual* | Yes |
529
+ | Perform a find request | Manual* | Yes |
530
+ | Set global field values | Manual* | Yes |
531
+ | Run a script | Manual* | Yes |
532
+ | Run a script with another request | Manual* | Yes |
488
533
 
489
534
  \* You can manually supply the URL and JSON to a `FmRest` connection.
490
535
 
@@ -519,6 +564,6 @@ See [LICENSE.txt](LICENSE.txt).
519
564
 
520
565
  ## Disclaimer
521
566
 
522
- This project is not sponsored by or otherwise affiliated with FileMaker, Inc,
523
- an Apple subsidiary. FileMaker is a trademark of FileMaker, Inc., registered in
524
- the U.S. and other countries.
567
+ This project is not sponsored by or otherwise affiliated with Claris
568
+ International Inc., an Apple Inc. subsidiary. FileMaker is a trademark of
569
+ 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
  #
@@ -76,13 +73,21 @@ module FmRest
76
73
  # Modifies the given hash in-place encoding non-string values (e.g.
77
74
  # dates) to their string representation when appropriate.
78
75
  #
76
+ # nil gets converted to empty string as the Data API doesn't accept
77
+ # nulls in JSON. Likewise, true and false get converted to 1 and 0
78
+ # respectively.
79
+ #
79
80
  def serialize_values!(params)
80
81
  params.transform_values! do |value|
81
82
  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)
83
+ when *FmRest::V1.datetime_classes
84
+ FmRest::V1.convert_datetime_timezone(value.to_datetime, fmrest_config.timezone).strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
85
+ when *FmRest::V1.date_classes
86
+ value.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
87
+ when nil
88
+ ""
89
+ when true, false
90
+ value ? 1 : 0
86
91
  else
87
92
  value
88
93
  end
@@ -90,25 +95,6 @@ module FmRest
90
95
 
91
96
  params
92
97
  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
98
  end
113
99
  end
114
100
  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.1
4
+ version: 0.16.0
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-16 00:00:00.000000000 Z
11
+ date: 2021-04-14 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.1
19
+ version: 0.16.0
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.1
26
+ version: 0.16.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: spyke
29
29
  requirement: !ruby/object:Gem::Requirement