fmrest 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +33 -0
- data/CHANGELOG.md +11 -0
- data/README.md +66 -1
- data/lib/fmrest.rb +8 -3
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +2 -0
- data/lib/fmrest/spyke/model/auth.rb +8 -0
- data/lib/fmrest/spyke/model/connection.rb +87 -26
- data/lib/fmrest/spyke/model/serialization.rb +1 -1
- data/lib/fmrest/token_store.rb +6 -0
- data/lib/fmrest/token_store/base.rb +3 -3
- data/lib/fmrest/v1.rb +6 -4
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +49 -25
- data/lib/fmrest/v1/raise_errors.rb +3 -1
- data/lib/fmrest/v1/token_session.rb +41 -49
- data/lib/fmrest/v1/type_coercer.rb +8 -8
- data/lib/fmrest/v1/utils.rb +0 -1
- data/lib/fmrest/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6433cf32d6d0111377f6080967e96fd5ff4cd54c5e223551c777a24c58e8662
|
4
|
+
data.tar.gz: 1a0fcd5951ab9b5bee88d822a429f16b02dcb01d5c95f74de9d529bb1ed00278
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0130f8b3598b3723d3c9e2514448a44a10381045176894cfed21244547c7ed41d0bc83fd18ab40981e9a2fc9ac7bd5c7290a5026b301878ea2c1d9ec440bb76
|
7
|
+
data.tar.gz: 0fc6a827bb57dea0b261bb812b58322dad8a01eecf78a4344bef877732f4f0b016636750833c048c5831e63e5015c707264a0697282cc7fba9a6879a0cd7e8b1
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: CI
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
test:
|
18
|
+
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
|
21
|
+
steps:
|
22
|
+
- uses: actions/checkout@v2
|
23
|
+
- name: Set up Ruby
|
24
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
25
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
26
|
+
# uses: ruby/setup-ruby@v1
|
27
|
+
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
|
28
|
+
with:
|
29
|
+
ruby-version: 2.6
|
30
|
+
- name: Install dependencies
|
31
|
+
run: bundle install
|
32
|
+
- name: Run specs
|
33
|
+
run: bundle exec rspec spec
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
### 0.11.0
|
4
|
+
|
5
|
+
* Added custom class for connection settings, providing indifferent access
|
6
|
+
(i.e. keys can be strings or symbols), and centralized default values and
|
7
|
+
validations
|
8
|
+
* Added `:autologin`, `:token` and `:token_store` connection settings
|
9
|
+
* Added `FmRest::Base.fmrest_config_overlay=` and related methods
|
10
|
+
* Added `FmRest::V1.request_auth_token` and
|
11
|
+
`FmRest::Spyke::Base.request_auth_token` (as well as `!`-suffixed versions
|
12
|
+
which raise exceptions on failure)
|
13
|
+
|
3
14
|
### 0.10.1
|
4
15
|
|
5
16
|
* Fix `URI.escape` obsolete warning messages in Ruby 2.7 by replacing it with
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# fmrest-ruby
|
2
2
|
|
3
|
-
|
3
|
+
[](https://rubygems.org/gems/fmrest)
|
4
|
+

|
4
5
|
|
5
6
|
A Ruby client for
|
6
7
|
[FileMaker 18 and 19's Data API](https://help.claris.com/en/data-api-guide)
|
@@ -121,6 +122,8 @@ Option | Description | Format
|
|
121
122
|
`:timestamp_format` | Timestmap parsing format | String (FM date format) | `"MM/dd/yyyy HH:mm:ss"`
|
122
123
|
`:time_format` | Time parsing format | String (FM date format) | `"HH:mm:ss"`
|
123
124
|
`:timezone` | The timezone for the FM server | `:local` \| `:utc` \| `nil` | `nil`
|
125
|
+
`:autologin` | Whether to automatically start Data API sessions | Boolean | `true`
|
126
|
+
`:token` | Used to manually provide a session token (e.g. if `:autologin` is `false`) | String | None
|
124
127
|
|
125
128
|
### Default connection settings
|
126
129
|
|
@@ -418,6 +421,59 @@ class Honeybee < BeeBase
|
|
418
421
|
end
|
419
422
|
```
|
420
423
|
|
424
|
+
### Model.fmrest_config_overlay=
|
425
|
+
|
426
|
+
There may be cases where you want to use different connection settings
|
427
|
+
depending on context, for example if you want to use username and password
|
428
|
+
provided by the user in a web application. Since `Model.fmrest_config` is
|
429
|
+
global, changing the username/password for one context would also change it for
|
430
|
+
all other contexts, leading to security issues.
|
431
|
+
|
432
|
+
`Model.fmrest_config_overlay=` solves that issue by allowing you to override
|
433
|
+
some settings in a thread-local and reversible manner. That way, using the same
|
434
|
+
example as above, you could connect to the Data API with user-provided
|
435
|
+
credentials without having them leak into other users of your web application.
|
436
|
+
|
437
|
+
E.g.:
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
class BeeBase < Spyke::Base
|
441
|
+
include FmRest::Spyke
|
442
|
+
|
443
|
+
# Host and database provided as base settings
|
444
|
+
self.fmrest_config = {
|
445
|
+
host: "example.com",
|
446
|
+
database: "My Database"
|
447
|
+
}
|
448
|
+
end
|
449
|
+
|
450
|
+
# E.g. in a controller-action of a Rails application:
|
451
|
+
|
452
|
+
# User-provided credentials
|
453
|
+
BeeBase.fmrest_config_overlay = {
|
454
|
+
username: params[:username],
|
455
|
+
password: params[:password]
|
456
|
+
}
|
457
|
+
|
458
|
+
# Perform some Data API requests ...
|
459
|
+
```
|
460
|
+
|
461
|
+
### Model.clear_fmrest_config_overlay
|
462
|
+
|
463
|
+
Clears the thread-local settings provided to `fmrest_config_overaly=`.
|
464
|
+
|
465
|
+
### Model.with_overlay
|
466
|
+
|
467
|
+
Runs a block with the given settings overlay, resetting them after the block
|
468
|
+
finishes running. It wraps execution in its own fiber, so it doesn't affect the
|
469
|
+
overlay of the currently-running thread.
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
Honeybee.with_overlay(username: "...", password: "...") do
|
473
|
+
Honeybee.query(...)
|
474
|
+
end
|
475
|
+
```
|
476
|
+
|
421
477
|
### Model.layout
|
422
478
|
|
423
479
|
Use `layout` to set the `:layout` part of API URLs, e.g.:
|
@@ -434,6 +490,15 @@ Data API models.
|
|
434
490
|
Note that you only need to set this if the name of the model and the name of
|
435
491
|
the layout differ, otherwise the default will just work.
|
436
492
|
|
493
|
+
### Model.request_auth_token
|
494
|
+
|
495
|
+
Requests a Data API session token using the connection settings in
|
496
|
+
`fmrest_config` and returns it if successful, otherwise returns `false`.
|
497
|
+
|
498
|
+
You normally don't need to use this method as fmrest-ruby will automatically
|
499
|
+
request and store session tokens for you (provided that `:autologin` is
|
500
|
+
`true`).
|
501
|
+
|
437
502
|
### Model.logout
|
438
503
|
|
439
504
|
Use `logout` to log out from the database session (you may call it on any model
|
data/lib/fmrest.rb
CHANGED
@@ -4,16 +4,21 @@ require "faraday"
|
|
4
4
|
require "faraday_middleware"
|
5
5
|
|
6
6
|
require "fmrest/version"
|
7
|
-
require "fmrest/
|
7
|
+
require "fmrest/connection_settings"
|
8
8
|
|
9
9
|
module FmRest
|
10
|
+
autoload :V1, "fmrest/v1"
|
11
|
+
autoload :TokenStore, "fmrest/token_store"
|
12
|
+
|
10
13
|
class << self
|
11
14
|
attr_accessor :token_store
|
12
15
|
|
13
|
-
|
16
|
+
def default_connection_settings=(settings)
|
17
|
+
@default_connection_settings = ConnectionSettings.wrap(settings, skip_validation: true)
|
18
|
+
end
|
14
19
|
|
15
20
|
def default_connection_settings
|
16
|
-
@default_connection_settings || {}
|
21
|
+
@default_connection_settings || ConnectionSettings.new({}, skip_validation: true)
|
17
22
|
end
|
18
23
|
|
19
24
|
def config=(connection_hash)
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
# Wrapper class for connection settings hash, with a number of purposes:
|
5
|
+
#
|
6
|
+
# * Provide indifferent access (base hash can have either string or symbol
|
7
|
+
# keys)
|
8
|
+
# * Method access
|
9
|
+
# * Default values
|
10
|
+
# * Basic validation
|
11
|
+
# * Normalization (e.g. aliased settings)
|
12
|
+
# * Useful error messages
|
13
|
+
class ConnectionSettings
|
14
|
+
class MissingSetting < ArgumentError; end
|
15
|
+
|
16
|
+
PROPERTIES = %i(
|
17
|
+
host
|
18
|
+
database
|
19
|
+
username
|
20
|
+
password
|
21
|
+
token
|
22
|
+
token_store
|
23
|
+
autologin
|
24
|
+
ssl
|
25
|
+
proxy
|
26
|
+
log
|
27
|
+
coerce_dates
|
28
|
+
date_format
|
29
|
+
timestamp_format
|
30
|
+
time_format
|
31
|
+
timezone
|
32
|
+
).freeze
|
33
|
+
|
34
|
+
# NOTE: password intentionally left non-required since it's only really
|
35
|
+
# needed when no token exists, and should only be required when logging in
|
36
|
+
REQUIRED = %i(
|
37
|
+
host
|
38
|
+
database
|
39
|
+
).freeze
|
40
|
+
|
41
|
+
DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
|
42
|
+
DEFAULT_TIME_FORMAT = "HH:mm:ss"
|
43
|
+
DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
|
44
|
+
|
45
|
+
DEFAULTS = {
|
46
|
+
autologin: true,
|
47
|
+
log: false,
|
48
|
+
date_format: DEFAULT_DATE_FORMAT,
|
49
|
+
time_format: DEFAULT_TIME_FORMAT,
|
50
|
+
timestamp_format: DEFAULT_TIMESTAMP_FORMAT,
|
51
|
+
coerce_dates: false
|
52
|
+
}.freeze
|
53
|
+
|
54
|
+
def self.wrap(settings, skip_validation: false)
|
55
|
+
if settings.kind_of?(self)
|
56
|
+
settings.validate unless skip_validation
|
57
|
+
return settings
|
58
|
+
end
|
59
|
+
new(settings, skip_validation: skip_validation)
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(settings, skip_validation: false)
|
63
|
+
@settings = settings.to_h.dup
|
64
|
+
normalize
|
65
|
+
validate unless skip_validation
|
66
|
+
end
|
67
|
+
|
68
|
+
PROPERTIES.each do |p|
|
69
|
+
define_method(p) do
|
70
|
+
get(p)
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method("#{p}!") do
|
74
|
+
r = get(p)
|
75
|
+
raise MissingSetting, "Missing required setting: `#{p}'" if r.nil?
|
76
|
+
r
|
77
|
+
end
|
78
|
+
|
79
|
+
define_method("#{p}?") do
|
80
|
+
!!get(p)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def [](key)
|
85
|
+
raise ArgumentError, "Unknown setting `#{key}'" unless PROPERTIES.include?(key.to_sym)
|
86
|
+
get(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_h
|
90
|
+
PROPERTIES.each_with_object({}) do |p, h|
|
91
|
+
v = get(p)
|
92
|
+
h[p] = v unless v == DEFAULTS[p]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def merge(other, **keyword_args)
|
97
|
+
other = self.class.wrap(other, skip_validation: true)
|
98
|
+
self.class.new(to_h.merge(other.to_h), **keyword_args)
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate
|
102
|
+
missing = REQUIRED.select { |r| get(r).nil? }.map { |m| "`#{m}'" }
|
103
|
+
raise MissingSetting, "Missing required setting(s): #{missing.join(', ')}" unless missing.empty?
|
104
|
+
|
105
|
+
unless username? || token?
|
106
|
+
raise MissingSetting, "A minimum of `username' or `token' are required to be able to establish a connection"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def get(key)
|
113
|
+
return @settings[key.to_sym] if @settings.has_key?(key.to_sym)
|
114
|
+
return @settings[key.to_s] if @settings.has_key?(key.to_s)
|
115
|
+
DEFAULTS[key.to_sym]
|
116
|
+
end
|
117
|
+
|
118
|
+
def normalize
|
119
|
+
if !get(:username) && account_name = get(:account_name)
|
120
|
+
@settings[:username] = account_name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/fmrest/errors.rb
CHANGED
@@ -21,6 +21,8 @@ module FmRest
|
|
21
21
|
class APIError::NoMatchingRecordsError < APIError::ParameterError; end
|
22
22
|
class APIError::ValidationError < APIError; end # error codes 500..599
|
23
23
|
class APIError::SystemError < APIError; end # error codes 800..899
|
24
|
+
class APIError::InvalidToken < APIError; end # error code 952
|
25
|
+
class APIError::MaximumDataAPICallsExceeded < APIError; end # error code 953
|
24
26
|
class APIError::ScriptError < APIError; end # error codes 1200..1299
|
25
27
|
class APIError::ODBCError < APIError; end # error codes 1400..1499
|
26
28
|
|
@@ -28,6 +28,14 @@ module FmRest
|
|
28
28
|
rescue FmRest::V1::TokenSession::NoSessionTokenSet
|
29
29
|
false
|
30
30
|
end
|
31
|
+
|
32
|
+
def request_auth_token
|
33
|
+
FmRest::V1.request_auth_token(FmRest::V1.auth_connection(fmrest_config))
|
34
|
+
end
|
35
|
+
|
36
|
+
def request_auth_token!
|
37
|
+
FmRest::V1.request_auth_token!(FmRest::V1.auth_connection(fmrest_config))
|
38
|
+
end
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -7,25 +7,67 @@ module FmRest
|
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
included do
|
10
|
-
class_attribute :fmrest_config, instance_writer: false, instance_predicate: false
|
11
|
-
|
12
|
-
# Overrides the fmrest_config reader created by class_attribute so we
|
13
|
-
# can default set the default at call time.
|
14
|
-
#
|
15
|
-
# This method gets overwriten in subclasses if self.fmrest_config= is
|
16
|
-
# called.
|
17
|
-
define_singleton_method(:fmrest_config) do
|
18
|
-
FmRest.default_connection_settings
|
19
|
-
end
|
20
|
-
|
21
10
|
class_attribute :faraday_block, instance_accessor: false, instance_predicate: false
|
22
11
|
class << self; private :faraday_block, :faraday_block=; end
|
23
12
|
|
24
|
-
# FM Data API expects PATCH for updates (Spyke's default
|
13
|
+
# FM Data API expects PATCH for updates (Spyke's default is PUT)
|
25
14
|
self.callback_methods = { create: :post, update: :patch }.freeze
|
26
15
|
end
|
27
16
|
|
28
17
|
class_methods do
|
18
|
+
def fmrest_config
|
19
|
+
if fmrest_config_overlay
|
20
|
+
return FmRest.default_connection_settings.merge(fmrest_config_overlay, skip_validation: true)
|
21
|
+
end
|
22
|
+
|
23
|
+
FmRest.default_connection_settings
|
24
|
+
end
|
25
|
+
|
26
|
+
# Behaves similar to ActiveSupport's class_attribute, redefining the
|
27
|
+
# reader method so it can be inherited and overwritten in subclasses
|
28
|
+
#
|
29
|
+
def fmrest_config=(settings)
|
30
|
+
settings = ConnectionSettings.new(settings, skip_validation: true)
|
31
|
+
|
32
|
+
redefine_singleton_method(:fmrest_config) do
|
33
|
+
overlay = fmrest_config_overlay
|
34
|
+
return settings.merge(overlay, skip_validation: true) if overlay
|
35
|
+
settings
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Allows overwriting some connection settings in a thread-local
|
40
|
+
# manner. Useful in the use case where you want to connect to the
|
41
|
+
# same database using different accounts (e.g. credentials provided
|
42
|
+
# by users in a web app context)
|
43
|
+
#
|
44
|
+
def fmrest_config_overlay=(settings)
|
45
|
+
Thread.current[fmrest_config_overlay_key] = settings
|
46
|
+
end
|
47
|
+
|
48
|
+
def fmrest_config_overlay
|
49
|
+
Thread.current[fmrest_config_overlay_key] || begin
|
50
|
+
superclass.fmrest_config_overlay
|
51
|
+
rescue NoMethodError
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_fmrest_config_overlay
|
57
|
+
Thread.current[fmrest_config_overlay_key] = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def with_overlay(settings, &block)
|
61
|
+
Fiber.new do
|
62
|
+
begin
|
63
|
+
self.fmrest_config_overlay = settings
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
self.clear_fmrest_config_overlay
|
67
|
+
end
|
68
|
+
end.resume
|
69
|
+
end
|
70
|
+
|
29
71
|
def connection
|
30
72
|
super || fmrest_connection
|
31
73
|
end
|
@@ -47,26 +89,45 @@ module FmRest
|
|
47
89
|
private
|
48
90
|
|
49
91
|
def fmrest_connection
|
50
|
-
|
51
|
-
begin
|
52
|
-
config = fmrest_config
|
92
|
+
memoize = false
|
53
93
|
|
54
|
-
|
55
|
-
|
94
|
+
# Don't memoize the connection if there's an overlay, since
|
95
|
+
# overlays are thread-local and so should be the connection
|
96
|
+
unless fmrest_config_overlay
|
97
|
+
return @fmrest_connection if @fmrest_connection
|
98
|
+
memoize = true
|
99
|
+
end
|
56
100
|
|
57
|
-
|
58
|
-
# access to extra context defined in the model, e.g. a portal
|
59
|
-
# where name of the portal and the attributes prefix don't match
|
60
|
-
# and need to be specified as options to `portal`
|
61
|
-
conn.use FmRest::Spyke::SpykeFormatter, self
|
101
|
+
config = ConnectionSettings.wrap(fmrest_config)
|
62
102
|
|
63
|
-
|
103
|
+
connection =
|
104
|
+
FmRest::V1.build_connection(config) do |conn|
|
105
|
+
faraday_block.call(conn) if faraday_block
|
64
106
|
|
65
|
-
|
66
|
-
|
67
|
-
|
107
|
+
# Pass the class to SpykeFormatter's initializer so it can have
|
108
|
+
# access to extra context defined in the model, e.g. a portal
|
109
|
+
# where name of the portal and the attributes prefix don't match
|
110
|
+
# and need to be specified as options to `portal`
|
111
|
+
conn.use FmRest::Spyke::SpykeFormatter, self
|
112
|
+
|
113
|
+
conn.use FmRest::V1::TypeCoercer, config
|
114
|
+
|
115
|
+
# FmRest::Spyke::JsonParse expects symbol keys
|
116
|
+
conn.response :json, parser_options: { symbolize_names: true }
|
68
117
|
end
|
118
|
+
|
119
|
+
@fmrest_connection = connection if memoize
|
120
|
+
|
121
|
+
connection
|
69
122
|
end
|
123
|
+
|
124
|
+
def fmrest_config_overlay_key
|
125
|
+
:"#{object_id}.fmrest_config_overlay"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def fmrest_config
|
130
|
+
self.class.fmrest_config
|
70
131
|
end
|
71
132
|
end
|
72
133
|
end
|
data/lib/fmrest/token_store.rb
CHANGED
@@ -2,5 +2,11 @@
|
|
2
2
|
|
3
3
|
module FmRest
|
4
4
|
module TokenStore
|
5
|
+
autoload :Base, "fmrest/token_store/base"
|
6
|
+
autoload :Memory, "fmrest/token_store/memory"
|
7
|
+
autoload :Null, "fmrest/token_store/null"
|
8
|
+
autoload :ActiveRecord, "fmrest/token_store/active_record"
|
9
|
+
autoload :Moneta, "fmrest/token_store/moneta"
|
10
|
+
autoload :Redis, "fmrest/token_store/redis"
|
5
11
|
end
|
6
12
|
end
|
@@ -10,15 +10,15 @@ module FmRest
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def load(key)
|
13
|
-
raise
|
13
|
+
raise NotImplementedError
|
14
14
|
end
|
15
15
|
|
16
16
|
def store(key, value)
|
17
|
-
raise
|
17
|
+
raise NotImplementedError
|
18
18
|
end
|
19
19
|
|
20
20
|
def delete(key)
|
21
|
-
raise
|
21
|
+
raise NotImplementedError
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
data/lib/fmrest/v1.rb
CHANGED
@@ -5,17 +5,19 @@ require "fmrest/v1/paths"
|
|
5
5
|
require "fmrest/v1/container_fields"
|
6
6
|
require "fmrest/v1/utils"
|
7
7
|
require "fmrest/v1/dates"
|
8
|
+
require "fmrest/v1/auth"
|
8
9
|
|
9
10
|
module FmRest
|
10
11
|
module V1
|
11
|
-
DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
|
12
|
-
DEFAULT_TIME_FORMAT = "HH:mm:ss"
|
13
|
-
DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
|
14
|
-
|
15
12
|
extend Connection
|
16
13
|
extend Paths
|
17
14
|
extend ContainerFields
|
18
15
|
extend Utils
|
19
16
|
extend Dates
|
17
|
+
extend Auth
|
18
|
+
|
19
|
+
autoload :TokenSession, "fmrest/v1/token_session"
|
20
|
+
autoload :RaiseErrors, "fmrest/v1/raise_errors"
|
21
|
+
autoload :TypeCoercer, "fmrest/v1/type_coercer"
|
20
22
|
end
|
21
23
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module V1
|
5
|
+
module Auth
|
6
|
+
# Requests a token through basic auth
|
7
|
+
#
|
8
|
+
# @param connection [Faraday] the auth connection to use for
|
9
|
+
# the request
|
10
|
+
# @return The token if successful
|
11
|
+
# @return `false` if authentication failed
|
12
|
+
def request_auth_token(connection = FmRest::V1.auth_connection)
|
13
|
+
request_auth_token!(connection)
|
14
|
+
rescue FmRest::APIError::AccountError
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
# Requests a token through basic auth, raising
|
19
|
+
# `FmRest::APIError::AccountError` if auth fails
|
20
|
+
#
|
21
|
+
# @param (see #request_auth_token)
|
22
|
+
# @return The token if successful
|
23
|
+
# @raise [FmRest::APIError::AccountError] if authentication failed
|
24
|
+
def request_auth_token!(connection = FmRest.V1.auth_connection)
|
25
|
+
resp = connection.post(V1.session_path)
|
26
|
+
resp.body["response"]["token"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/fmrest/v1/connection.rb
CHANGED
@@ -7,21 +7,21 @@ module FmRest
|
|
7
7
|
module Connection
|
8
8
|
BASE_PATH = "/fmi/data/v1/databases"
|
9
9
|
|
10
|
+
AUTH_HEADERS = { "Content-Type" => "application/json" }.freeze
|
11
|
+
|
10
12
|
# Builds a complete DAPI Faraday connection with middleware already
|
11
13
|
# configured to handle authentication, JSON parsing, logging and DAPI
|
12
14
|
# error handling. A block can be optionally given for additional
|
13
15
|
# middleware configuration
|
14
16
|
#
|
15
|
-
# @option options [String] :username The username for DAPI authentication
|
16
|
-
# @option options [String] :account_name Alias of :username for
|
17
|
-
# compatibility with Rfm gem
|
18
|
-
# @option options [String] :password The password for DAPI authentication
|
19
17
|
# @option (see #base_connection)
|
20
18
|
# @return (see #base_connection)
|
21
|
-
def build_connection(
|
22
|
-
|
19
|
+
def build_connection(settings = FmRest.default_connection_settings, &block)
|
20
|
+
settings = ConnectionSettings.wrap(settings)
|
21
|
+
|
22
|
+
base_connection(settings) do |conn|
|
23
23
|
conn.use RaiseErrors
|
24
|
-
conn.use TokenSession,
|
24
|
+
conn.use TokenSession, settings
|
25
25
|
|
26
26
|
# The EncodeJson and Multipart middlewares only encode the request
|
27
27
|
# when the content type matches, so we can have them both here and
|
@@ -33,32 +33,60 @@ module FmRest
|
|
33
33
|
|
34
34
|
# Allow overriding the default response middleware
|
35
35
|
if block_given?
|
36
|
-
yield conn,
|
36
|
+
yield conn, settings
|
37
37
|
else
|
38
|
-
conn.use TypeCoercer,
|
38
|
+
conn.use TypeCoercer, settings
|
39
39
|
conn.response :json
|
40
40
|
end
|
41
41
|
|
42
|
-
if
|
42
|
+
if settings.log
|
43
|
+
conn.response :logger, nil, bodies: true, headers: true
|
44
|
+
end
|
45
|
+
|
46
|
+
conn.adapter Faraday.default_adapter
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Builds a Faraday connection to use for DAPI basic auth login
|
51
|
+
#
|
52
|
+
# @option (see #base_connection)
|
53
|
+
# @return (see #base_connection)
|
54
|
+
def auth_connection(settings = FmRest.default_connection_settings)
|
55
|
+
settings = ConnectionSettings.wrap(settings)
|
56
|
+
|
57
|
+
base_connection(settings, { headers: AUTH_HEADERS }) do |conn|
|
58
|
+
conn.use RaiseErrors
|
59
|
+
|
60
|
+
conn.basic_auth settings.username!, settings.password!
|
61
|
+
|
62
|
+
if settings.log
|
43
63
|
conn.response :logger, nil, bodies: true, headers: true
|
44
64
|
end
|
45
65
|
|
66
|
+
conn.response :json
|
46
67
|
conn.adapter Faraday.default_adapter
|
47
68
|
end
|
48
69
|
end
|
49
70
|
|
50
71
|
# Builds a base Faraday connection with base URL constructed from
|
51
|
-
# connection
|
72
|
+
# connection settings and passes it the given block
|
52
73
|
#
|
53
|
-
# @option
|
54
|
-
# @option
|
55
|
-
# @option
|
74
|
+
# @option settings [String] :host The hostname for the FM server
|
75
|
+
# @option settings [String] :database The FM database name
|
76
|
+
# @option settings [String] :username The username for DAPI authentication
|
77
|
+
# @option settings [String] :account_name Alias of :username for
|
78
|
+
# compatibility with Rfm gem
|
79
|
+
# @option settings [String] :password The password for DAPI authentication
|
80
|
+
# @option settings [String] :ssl SSL settings to forward to the Faraday
|
56
81
|
# connection
|
57
|
-
# @option
|
82
|
+
# @option settings [String] :proxy Proxy options to forward to the Faraday
|
58
83
|
# connection
|
84
|
+
# @param faraday_options [Hash] additional options for Faraday object
|
59
85
|
# @return [Faraday] The new Faraday connection
|
60
|
-
def base_connection(
|
61
|
-
|
86
|
+
def base_connection(settings = FmRest.default_connection_settings, faraday_options = nil, &block)
|
87
|
+
settings = ConnectionSettings.wrap(settings)
|
88
|
+
|
89
|
+
host = settings.host!
|
62
90
|
|
63
91
|
# Default to HTTPS
|
64
92
|
scheme = "https"
|
@@ -70,11 +98,11 @@ module FmRest
|
|
70
98
|
scheme = uri.scheme
|
71
99
|
end
|
72
100
|
|
73
|
-
faraday_options = {}
|
74
|
-
faraday_options[:ssl] =
|
75
|
-
faraday_options[:proxy] =
|
101
|
+
faraday_options = (faraday_options || {}).dup
|
102
|
+
faraday_options[:ssl] = settings.ssl if settings.ssl?
|
103
|
+
faraday_options[:proxy] = settings.proxy if settings.proxy?
|
76
104
|
|
77
|
-
database = URI.encode_www_form_component(
|
105
|
+
database = URI.encode_www_form_component(settings.database!)
|
78
106
|
|
79
107
|
Faraday.new(
|
80
108
|
"#{scheme}://#{host}#{BASE_PATH}/#{database}/".freeze,
|
@@ -85,7 +113,3 @@ module FmRest
|
|
85
113
|
end
|
86
114
|
end
|
87
115
|
end
|
88
|
-
|
89
|
-
require "fmrest/v1/token_session"
|
90
|
-
require "fmrest/v1/raise_errors"
|
91
|
-
require "fmrest/v1/type_coercer"
|
@@ -23,9 +23,11 @@ module FmRest
|
|
23
23
|
402..499 => APIError::ParameterError,
|
24
24
|
500..599 => APIError::ValidationError,
|
25
25
|
800..899 => APIError::SystemError,
|
26
|
+
952 => APIError::InvalidToken,
|
27
|
+
953 => APIError::MaximumDataAPICallsExceeded,
|
26
28
|
1200..1299 => APIError::ScriptError,
|
27
29
|
1400..1499 => APIError::ODBCError
|
28
|
-
}
|
30
|
+
}.freeze
|
29
31
|
|
30
32
|
def on_complete(env)
|
31
33
|
# Sniff for either straight JSON parsing or Spyke's format
|
@@ -10,15 +10,15 @@ module FmRest
|
|
10
10
|
class TokenSession < Faraday::Middleware
|
11
11
|
class NoSessionTokenSet < FmRest::Error; end
|
12
12
|
|
13
|
-
HEADER_KEY = "Authorization"
|
13
|
+
HEADER_KEY = "Authorization"
|
14
14
|
TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze
|
15
15
|
LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::BASE_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze
|
16
16
|
|
17
17
|
# @param app [#call]
|
18
|
-
# @param
|
19
|
-
def initialize(app,
|
18
|
+
# @param settings [FmRest::ConnectionSettings]
|
19
|
+
def initialize(app, settings)
|
20
20
|
super(app)
|
21
|
-
@
|
21
|
+
@settings = settings
|
22
22
|
end
|
23
23
|
|
24
24
|
# Entry point for the middleware when sending a request
|
@@ -32,27 +32,37 @@ module FmRest
|
|
32
32
|
|
33
33
|
@app.call(env).on_complete do |response_env|
|
34
34
|
if response_env[:status] == 401 # Unauthorized
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
delete_token_store_key
|
36
|
+
|
37
|
+
if @settings.autologin
|
38
|
+
env[:body] = request_body
|
39
|
+
set_auth_header(env)
|
40
|
+
return @app.call(env)
|
41
|
+
end
|
39
42
|
end
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
43
46
|
private
|
44
47
|
|
48
|
+
def delete_token_store_key
|
49
|
+
token_store.delete(token_store_key)
|
50
|
+
# Sometimes we may want to pass the :token in settings manually, and
|
51
|
+
# refrain from passing a :username. In that case the call to
|
52
|
+
# #token_store_key above would fail as it tries to fetch :username, so
|
53
|
+
# we purposely ignore that error.
|
54
|
+
rescue FmRest::ConnectionSettings::MissingSetting
|
55
|
+
end
|
56
|
+
|
45
57
|
def handle_logout(env)
|
46
|
-
token = token_store.load(token_store_key)
|
58
|
+
token = @settings.token? ? @settings.token : token_store.load(token_store_key)
|
47
59
|
|
48
60
|
raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token
|
49
61
|
|
50
62
|
env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}")
|
51
63
|
|
52
64
|
@app.call(env).on_complete do |response_env|
|
53
|
-
if response_env[:status] == 200
|
54
|
-
token_store.delete(token_store_key)
|
55
|
-
end
|
65
|
+
delete_token_store_key if response_env[:status] == 200
|
56
66
|
end
|
57
67
|
end
|
58
68
|
|
@@ -65,43 +75,33 @@ module FmRest
|
|
65
75
|
env.request_headers[HEADER_KEY] = "Bearer #{token}"
|
66
76
|
end
|
67
77
|
|
68
|
-
#
|
78
|
+
# Uses the token given in connection settings if available,
|
79
|
+
# otherwisek tries to get an existing token from the token store,
|
69
80
|
# otherwise requests one through basic auth,
|
70
81
|
# otherwise raises an exception.
|
71
82
|
#
|
72
83
|
def token
|
84
|
+
return @settings.token if @settings.token?
|
85
|
+
|
73
86
|
token = token_store.load(token_store_key)
|
74
87
|
return token if token
|
75
88
|
|
76
|
-
|
77
|
-
token_store.store(token_store_key, token)
|
78
|
-
return token
|
79
|
-
end
|
89
|
+
return nil unless @settings.autologin
|
80
90
|
|
81
|
-
|
82
|
-
|
91
|
+
token = V1.request_auth_token!(auth_connection)
|
92
|
+
token_store.store(token_store_key, token)
|
93
|
+
token
|
83
94
|
end
|
84
95
|
|
85
|
-
#
|
86
|
-
#
|
87
|
-
def request_token
|
88
|
-
resp = auth_connection.post do |req|
|
89
|
-
req.url V1.session_path
|
90
|
-
req.headers["Content-Type"] = "application/json"
|
91
|
-
end
|
92
|
-
return resp.body["response"]["token"] if resp.success?
|
93
|
-
false
|
94
|
-
end
|
95
|
-
|
96
|
-
# The key to use to store a token, uses the format host:database
|
96
|
+
# The key to use to store a token, uses the format host:database:username
|
97
97
|
#
|
98
98
|
def token_store_key
|
99
99
|
@token_store_key ||=
|
100
100
|
begin
|
101
101
|
# Strip the host part to just the hostname (i.e. no scheme or port)
|
102
|
-
host = @
|
102
|
+
host = @settings.host!
|
103
103
|
host = URI(host).hostname if host =~ /\Ahttps?:\/\//
|
104
|
-
"#{host}:#{@
|
104
|
+
"#{host}:#{@settings.database!}:#{@settings.username!}"
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
@@ -111,31 +111,23 @@ module FmRest
|
|
111
111
|
if TOKEN_STORE_INTERFACE.all? { |method| token_store_option.respond_to?(method) }
|
112
112
|
token_store_option
|
113
113
|
elsif token_store_option.kind_of?(Class)
|
114
|
-
token_store_option.
|
114
|
+
if token_store_option.respond_to?(:instance)
|
115
|
+
token_store_option.instance
|
116
|
+
else
|
117
|
+
token_store_option.new
|
118
|
+
end
|
115
119
|
else
|
116
|
-
|
117
|
-
TokenStore::Memory.new
|
120
|
+
FmRest::TokenStore::Memory.new
|
118
121
|
end
|
119
122
|
end
|
120
123
|
end
|
121
124
|
|
122
125
|
def token_store_option
|
123
|
-
@
|
126
|
+
@settings.token_store || FmRest.token_store
|
124
127
|
end
|
125
128
|
|
126
129
|
def auth_connection
|
127
|
-
@auth_connection ||= V1.
|
128
|
-
username = @options.fetch(:account_name) { @options.fetch(:username) }
|
129
|
-
|
130
|
-
conn.basic_auth username, @options.fetch(:password)
|
131
|
-
|
132
|
-
if @options[:log]
|
133
|
-
conn.response :logger, nil, bodies: true, headers: true
|
134
|
-
end
|
135
|
-
|
136
|
-
conn.response :json
|
137
|
-
conn.adapter Faraday.default_adapter
|
138
|
-
end
|
130
|
+
@auth_connection ||= V1.auth_connection(@settings)
|
139
131
|
end
|
140
132
|
end
|
141
133
|
end
|
@@ -13,10 +13,10 @@ module FmRest
|
|
13
13
|
COERCE_FULL = [:full, "full"].freeze
|
14
14
|
|
15
15
|
# @param app [#call]
|
16
|
-
# @param
|
17
|
-
def initialize(app,
|
16
|
+
# @param settings [FmRest::ConnectionSettings]
|
17
|
+
def initialize(app, settings)
|
18
18
|
super(app)
|
19
|
-
@
|
19
|
+
@settings = settings
|
20
20
|
end
|
21
21
|
|
22
22
|
def on_complete(env)
|
@@ -112,15 +112,15 @@ module FmRest
|
|
112
112
|
end
|
113
113
|
|
114
114
|
def date_fm_format
|
115
|
-
@
|
115
|
+
@settings.date_format
|
116
116
|
end
|
117
117
|
|
118
118
|
def timestamp_fm_format
|
119
|
-
@
|
119
|
+
@settings.timestamp_format
|
120
120
|
end
|
121
121
|
|
122
122
|
def time_fm_format
|
123
|
-
@
|
123
|
+
@settings.time_format
|
124
124
|
end
|
125
125
|
|
126
126
|
def date_strptime_format
|
@@ -179,11 +179,11 @@ module FmRest
|
|
179
179
|
end
|
180
180
|
|
181
181
|
def local_timezone?
|
182
|
-
@local_timezone ||= @
|
182
|
+
@local_timezone ||= @settings.timezone.try(:to_sym) == :local
|
183
183
|
end
|
184
184
|
|
185
185
|
def coerce_dates
|
186
|
-
@
|
186
|
+
@settings.coerce_dates
|
187
187
|
end
|
188
188
|
|
189
189
|
alias_method :enabled?, :coerce_dates
|
data/lib/fmrest/v1/utils.rb
CHANGED
data/lib/fmrest/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fmrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Carbajal
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -226,6 +226,7 @@ executables: []
|
|
226
226
|
extensions: []
|
227
227
|
extra_rdoc_files: []
|
228
228
|
files:
|
229
|
+
- ".github/workflows/ci.yml"
|
229
230
|
- ".gitignore"
|
230
231
|
- ".rspec"
|
231
232
|
- ".travis.yml"
|
@@ -237,6 +238,7 @@ files:
|
|
237
238
|
- Rakefile
|
238
239
|
- fmrest.gemspec
|
239
240
|
- lib/fmrest.rb
|
241
|
+
- lib/fmrest/connection_settings.rb
|
240
242
|
- lib/fmrest/errors.rb
|
241
243
|
- lib/fmrest/spyke.rb
|
242
244
|
- lib/fmrest/spyke/base.rb
|
@@ -264,6 +266,7 @@ files:
|
|
264
266
|
- lib/fmrest/token_store/moneta.rb
|
265
267
|
- lib/fmrest/token_store/redis.rb
|
266
268
|
- lib/fmrest/v1.rb
|
269
|
+
- lib/fmrest/v1/auth.rb
|
267
270
|
- lib/fmrest/v1/connection.rb
|
268
271
|
- lib/fmrest/v1/container_fields.rb
|
269
272
|
- lib/fmrest/v1/dates.rb
|