fmrest 0.10.1 → 0.11.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: ccf62593af664237c1cd0c67928c869ae74cdcde4b2e3c9bd82c20fa0f6b0354
4
- data.tar.gz: 9f6bd645aed2551d278b0b31e194dc97726e66396a76148ee7dae3bca5c67a93
3
+ metadata.gz: e6433cf32d6d0111377f6080967e96fd5ff4cd54c5e223551c777a24c58e8662
4
+ data.tar.gz: 1a0fcd5951ab9b5bee88d822a429f16b02dcb01d5c95f74de9d529bb1ed00278
5
5
  SHA512:
6
- metadata.gz: 421a3be4119f862e31787b6027c7824e42641d85d6dbdcb6d5df2b849ef3e93556a03060a74e093a11fed962cdfbe61be19833c5d2195f6b4dad121ef759aeb6
7
- data.tar.gz: 7bd1359945a7da96afdfd05c8948148bc6c8740351eb7c4dd1a31ba501fc82f5ad79dd68a166b2fee3022c636fddd187f0b27b97fc2d645922afddb813f98787
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
@@ -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
- <a href="https://rubygems.org/gems/fmrest"><img src="https://badge.fury.io/rb/fmrest.svg?style=flat" alt="Gem Version"></a>
3
+ [![Gem Version](https://badge.fury.io/rb/fmrest.svg?style=flat)](https://rubygems.org/gems/fmrest)
4
+ ![CI](https://github.com/beezwax/fmrest-ruby/workflows/CI/badge.svg)
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
@@ -4,16 +4,21 @@ require "faraday"
4
4
  require "faraday_middleware"
5
5
 
6
6
  require "fmrest/version"
7
- require "fmrest/v1"
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
- attr_writer :default_connection_settings
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
@@ -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 was PUT)
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
- @fmrest_connection ||=
51
- begin
52
- config = fmrest_config
92
+ memoize = false
53
93
 
54
- FmRest::V1.build_connection(config) do |conn|
55
- faraday_block.call(conn) if faraday_block
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
- # Pass the class to SpykeFormatter's initializer so it can have
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
- conn.use FmRest::V1::TypeCoercer, config
103
+ connection =
104
+ FmRest::V1.build_connection(config) do |conn|
105
+ faraday_block.call(conn) if faraday_block
64
106
 
65
- # FmRest::Spyke::JsonParse expects symbol keys
66
- conn.response :json, parser_options: { symbolize_names: true }
67
- end
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
@@ -76,7 +76,7 @@ module FmRest
76
76
  end
77
77
 
78
78
  def convert_datetime_timezone(dt)
79
- case fmrest_config.fetch(:timezone, nil)
79
+ case fmrest_config.timezone
80
80
  when :utc, "utc"
81
81
  dt.new_offset(0)
82
82
  when :local, "local"
@@ -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 "Not implemented"
13
+ raise NotImplementedError
14
14
  end
15
15
 
16
16
  def store(key, value)
17
- raise "Not implemented"
17
+ raise NotImplementedError
18
18
  end
19
19
 
20
20
  def delete(key)
21
- raise "Not implemented"
21
+ raise NotImplementedError
22
22
  end
23
23
  end
24
24
  end
@@ -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
@@ -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(options = FmRest.default_connection_settings, &block)
22
- base_connection(options) do |conn|
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, options
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, options
36
+ yield conn, settings
37
37
  else
38
- conn.use TypeCoercer, options
38
+ conn.use TypeCoercer, settings
39
39
  conn.response :json
40
40
  end
41
41
 
42
- if options[:log]
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 options and passes it the given block
72
+ # connection settings and passes it the given block
52
73
  #
53
- # @option options [String] :host The hostname for the FM server
54
- # @option options [String] :database The FM database name
55
- # @option options [String] :ssl SSL options to forward to the Faraday
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 options [String] :proxy Proxy options to forward to the Faraday
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(options = FmRest.default_connection_settings, &block)
61
- host = options.fetch(:host)
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] = options[:ssl] if options.key?(:ssl)
75
- faraday_options[:proxy] = options[:proxy] if options.key?(: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(options.fetch(:database))
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".freeze
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 options [Hash]
19
- def initialize(app, options = FmRest.default_connection_settings)
18
+ # @param settings [FmRest::ConnectionSettings]
19
+ def initialize(app, settings)
20
20
  super(app)
21
- @options = options
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
- env[:body] = request_body
36
- token_store.delete(token_store_key)
37
- set_auth_header(env)
38
- return @app.call(env)
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
- # Tries to get an existing token from the token store,
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
- if token = request_token
77
- token_store.store(token_store_key, token)
78
- return token
79
- end
89
+ return nil unless @settings.autologin
80
90
 
81
- # TODO: Make this a custom exception class
82
- raise "Filemaker auth failed"
91
+ token = V1.request_auth_token!(auth_connection)
92
+ token_store.store(token_store_key, token)
93
+ token
83
94
  end
84
95
 
85
- # Requests a token through basic auth
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 = @options.fetch(:host)
102
+ host = @settings.host!
103
103
  host = URI(host).hostname if host =~ /\Ahttps?:\/\//
104
- "#{host}:#{@options.fetch(:database)}"
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.new
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
- require "fmrest/token_store/memory"
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
- @options[:token_store] || FmRest.token_store
126
+ @settings.token_store || FmRest.token_store
124
127
  end
125
128
 
126
129
  def auth_connection
127
- @auth_connection ||= V1.base_connection(@options) do |conn|
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 options [Hash]
17
- def initialize(app, options = FmRest.default_connection_settings)
16
+ # @param settings [FmRest::ConnectionSettings]
17
+ def initialize(app, settings)
18
18
  super(app)
19
- @options = options
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
- @options[:date_format] || DEFAULT_DATE_FORMAT
115
+ @settings.date_format
116
116
  end
117
117
 
118
118
  def timestamp_fm_format
119
- @options[:timestamp_format] || DEFAULT_TIMESTAMP_FORMAT
119
+ @settings.timestamp_format
120
120
  end
121
121
 
122
122
  def time_fm_format
123
- @options[:time_format] || DEFAULT_TIME_FORMAT
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 ||= @options.fetch(:timezone, nil).try(:to_sym) == :local
182
+ @local_timezone ||= @settings.timezone.try(:to_sym) == :local
183
183
  end
184
184
 
185
185
  def coerce_dates
186
- @options.fetch(:coerce_dates, false)
186
+ @settings.coerce_dates
187
187
  end
188
188
 
189
189
  alias_method :enabled?, :coerce_dates
@@ -72,7 +72,6 @@ module FmRest
72
72
  params
73
73
  end
74
74
 
75
-
76
75
  private
77
76
 
78
77
  def convert_script_arguments(script_arguments, suffix = nil)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FmRest
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.0"
5
5
  end
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.10.1
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-08-15 00:00:00.000000000 Z
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