fmrest 0.9.0 → 0.12.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/CHANGELOG.md +35 -0
- data/README.md +94 -17
- data/UPGRADING +15 -0
- data/fmrest.gemspec +16 -5
- data/lib/fmrest.rb +10 -3
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +2 -0
- data/lib/fmrest/spyke/base.rb +0 -12
- data/lib/fmrest/spyke/container_field.rb +2 -2
- data/lib/fmrest/spyke/model.rb +3 -6
- data/lib/fmrest/spyke/model/associations.rb +15 -11
- data/lib/fmrest/spyke/model/attributes.rb +21 -29
- data/lib/fmrest/spyke/model/auth.rb +8 -0
- data/lib/fmrest/spyke/model/connection.rb +122 -24
- data/lib/fmrest/spyke/model/container_fields.rb +15 -0
- data/lib/fmrest/spyke/model/http.rb +42 -2
- data/lib/fmrest/spyke/model/orm.rb +61 -17
- data/lib/fmrest/spyke/model/record_id.rb +78 -0
- data/lib/fmrest/spyke/model/serialization.rb +26 -7
- data/lib/fmrest/spyke/model/uri.rb +3 -4
- data/lib/fmrest/spyke/spyke_formatter.rb +5 -5
- data/lib/fmrest/string_date.rb +46 -7
- data/lib/fmrest/token_store.rb +6 -0
- data/lib/fmrest/token_store/base.rb +3 -3
- data/lib/fmrest/token_store/null.rb +20 -0
- data/lib/fmrest/v1.rb +8 -4
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +51 -25
- data/lib/fmrest/v1/dates.rb +81 -0
- data/lib/fmrest/v1/raise_errors.rb +3 -3
- data/lib/fmrest/v1/token_session.rb +41 -50
- data/lib/fmrest/v1/type_coercer.rb +111 -36
- data/lib/fmrest/v1/utils.rb +0 -17
- data/lib/fmrest/version.rb +1 -1
- metadata +41 -19
data/lib/fmrest/v1.rb
CHANGED
@@ -4,16 +4,20 @@ require "fmrest/v1/connection"
|
|
4
4
|
require "fmrest/v1/paths"
|
5
5
|
require "fmrest/v1/container_fields"
|
6
6
|
require "fmrest/v1/utils"
|
7
|
+
require "fmrest/v1/dates"
|
8
|
+
require "fmrest/v1/auth"
|
7
9
|
|
8
10
|
module FmRest
|
9
11
|
module V1
|
10
|
-
DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
|
11
|
-
DEFAULT_TIME_FORMAT = "HH:mm:ss"
|
12
|
-
DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
|
13
|
-
|
14
12
|
extend Connection
|
15
13
|
extend Paths
|
16
14
|
extend ContainerFields
|
17
15
|
extend Utils
|
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"
|
18
22
|
end
|
19
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,13 +33,13 @@ 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
43
|
conn.response :logger, nil, bodies: true, headers: true
|
44
44
|
end
|
45
45
|
|
@@ -47,18 +47,46 @@ module FmRest
|
|
47
47
|
end
|
48
48
|
end
|
49
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
|
63
|
+
conn.response :logger, nil, bodies: true, headers: true
|
64
|
+
end
|
65
|
+
|
66
|
+
conn.response :json
|
67
|
+
conn.adapter Faraday.default_adapter
|
68
|
+
end
|
69
|
+
end
|
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,12 +98,14 @@ 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?
|
104
|
+
|
105
|
+
database = URI.encode_www_form_component(settings.database!)
|
76
106
|
|
77
107
|
Faraday.new(
|
78
|
-
"#{scheme}://#{host}#{BASE_PATH}/#{
|
108
|
+
"#{scheme}://#{host}#{BASE_PATH}/#{database}/".freeze,
|
79
109
|
faraday_options,
|
80
110
|
&block
|
81
111
|
)
|
@@ -83,7 +113,3 @@ module FmRest
|
|
83
113
|
end
|
84
114
|
end
|
85
115
|
end
|
86
|
-
|
87
|
-
require "fmrest/v1/token_session"
|
88
|
-
require "fmrest/v1/raise_errors"
|
89
|
-
require "fmrest/v1/type_coercer"
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module V1
|
5
|
+
module Dates
|
6
|
+
FM_DATETIME_FORMAT_MATCHER = /MM|mm|dd|HH|ss|yyyy/.freeze
|
7
|
+
|
8
|
+
FM_DATE_TO_STRPTIME_SUBSTITUTIONS = {
|
9
|
+
"MM" => "%m",
|
10
|
+
"dd" => "%d",
|
11
|
+
"yyyy" => "%Y",
|
12
|
+
"HH" => "%H",
|
13
|
+
"mm" => "%M",
|
14
|
+
"ss" => "%S"
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
FM_DATE_TO_REGEXP_SUBSTITUTIONS = {
|
18
|
+
"MM" => '(?:0[1-9]|1[012])',
|
19
|
+
"dd" => '(?:0[1-9]|[12][0-9]|3[01])',
|
20
|
+
"yyyy" => '\d{4}',
|
21
|
+
"HH" => '(?:[01]\d|2[0123])',
|
22
|
+
"mm" => '[0-5]\d',
|
23
|
+
"ss" => '[0-5]\d'
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def self.extended(mod)
|
27
|
+
mod.instance_eval do
|
28
|
+
@date_strptime = {}
|
29
|
+
@date_regexp = {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Converts a FM date-time format to `DateTime.strptime` format
|
34
|
+
#
|
35
|
+
# @param fm_format [String] The FileMaker date-time format
|
36
|
+
# @return [String] The `DateTime.strpdate` equivalent of the given FM
|
37
|
+
# date-time format
|
38
|
+
def fm_date_to_strptime_format(fm_format)
|
39
|
+
@date_strptime[fm_format] ||=
|
40
|
+
fm_format.gsub(FM_DATETIME_FORMAT_MATCHER, FM_DATE_TO_STRPTIME_SUBSTITUTIONS).freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
# Converts a FM date-time format to a Regexp. This is mostly used a
|
44
|
+
# quicker way of checking whether a FM field is a date field than
|
45
|
+
# Date|DateTime.strptime
|
46
|
+
#
|
47
|
+
# @param fm_format [String] The FileMaker date-time format
|
48
|
+
# @return [Regexp] A reegular expression matching strings in the given FM
|
49
|
+
# date-time format
|
50
|
+
def fm_date_to_regexp(fm_format)
|
51
|
+
@date_regexp[fm_format] ||=
|
52
|
+
Regexp.new('\A' + fm_format.gsub(FM_DATETIME_FORMAT_MATCHER, FM_DATE_TO_REGEXP_SUBSTITUTIONS) + '\Z').freeze
|
53
|
+
end
|
54
|
+
|
55
|
+
# Takes a DateTime dt, and returns the correct local offset for that dt,
|
56
|
+
# daylight savings included, in fraction of a day.
|
57
|
+
#
|
58
|
+
# By default, if ActiveSupport's Time.zone is set it will be used instead
|
59
|
+
# of the system timezone.
|
60
|
+
#
|
61
|
+
# @param dt [DateTime] The DateTime to get the offset for
|
62
|
+
# @param zone [nil, String, TimeZone] The timezone to use to calculate
|
63
|
+
# the offset (defaults to system timezone, or ActiveSupport's Time.zone
|
64
|
+
# if set)
|
65
|
+
# @return [Rational] The offset in fraction of a day
|
66
|
+
def local_offset_for_datetime(dt, zone = nil)
|
67
|
+
dt = dt.new_offset(0)
|
68
|
+
time = ::Time.utc(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec)
|
69
|
+
|
70
|
+
# Do we have ActiveSupport's TimeZone?
|
71
|
+
time = if time.respond_to?(:in_time_zone)
|
72
|
+
time.in_time_zone(zone || ::Time.zone)
|
73
|
+
else
|
74
|
+
time.localtime
|
75
|
+
end
|
76
|
+
|
77
|
+
Rational(time.utc_offset, 86400) # seconds in one day (24*60*60)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fmrest/errors"
|
4
|
-
|
5
3
|
module FmRest
|
6
4
|
module V1
|
7
5
|
# FM Data API response middleware for raising exceptions on API response
|
@@ -23,9 +21,11 @@ module FmRest
|
|
23
21
|
402..499 => APIError::ParameterError,
|
24
22
|
500..599 => APIError::ValidationError,
|
25
23
|
800..899 => APIError::SystemError,
|
24
|
+
952 => APIError::InvalidToken,
|
25
|
+
953 => APIError::MaximumDataAPICallsExceeded,
|
26
26
|
1200..1299 => APIError::ScriptError,
|
27
27
|
1400..1499 => APIError::ODBCError
|
28
|
-
}
|
28
|
+
}.freeze
|
29
29
|
|
30
30
|
def on_complete(env)
|
31
31
|
# Sniff for either straight JSON parsing or Spyke's format
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "fmrest/v1/connection"
|
4
|
-
require "fmrest/errors"
|
5
4
|
|
6
5
|
module FmRest
|
7
6
|
module V1
|
@@ -10,15 +9,15 @@ module FmRest
|
|
10
9
|
class TokenSession < Faraday::Middleware
|
11
10
|
class NoSessionTokenSet < FmRest::Error; end
|
12
11
|
|
13
|
-
HEADER_KEY = "Authorization"
|
12
|
+
HEADER_KEY = "Authorization"
|
14
13
|
TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze
|
15
14
|
LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::BASE_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze
|
16
15
|
|
17
16
|
# @param app [#call]
|
18
|
-
# @param
|
19
|
-
def initialize(app,
|
17
|
+
# @param settings [FmRest::ConnectionSettings]
|
18
|
+
def initialize(app, settings)
|
20
19
|
super(app)
|
21
|
-
@
|
20
|
+
@settings = settings
|
22
21
|
end
|
23
22
|
|
24
23
|
# Entry point for the middleware when sending a request
|
@@ -32,27 +31,37 @@ module FmRest
|
|
32
31
|
|
33
32
|
@app.call(env).on_complete do |response_env|
|
34
33
|
if response_env[:status] == 401 # Unauthorized
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
delete_token_store_key
|
35
|
+
|
36
|
+
if @settings.autologin
|
37
|
+
env[:body] = request_body
|
38
|
+
set_auth_header(env)
|
39
|
+
return @app.call(env)
|
40
|
+
end
|
39
41
|
end
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
43
45
|
private
|
44
46
|
|
47
|
+
def delete_token_store_key
|
48
|
+
token_store.delete(token_store_key)
|
49
|
+
# Sometimes we may want to pass the :token in settings manually, and
|
50
|
+
# refrain from passing a :username. In that case the call to
|
51
|
+
# #token_store_key above would fail as it tries to fetch :username, so
|
52
|
+
# we purposely ignore that error.
|
53
|
+
rescue FmRest::ConnectionSettings::MissingSetting
|
54
|
+
end
|
55
|
+
|
45
56
|
def handle_logout(env)
|
46
|
-
token = token_store.load(token_store_key)
|
57
|
+
token = @settings.token? ? @settings.token : token_store.load(token_store_key)
|
47
58
|
|
48
59
|
raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token
|
49
60
|
|
50
61
|
env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}")
|
51
62
|
|
52
63
|
@app.call(env).on_complete do |response_env|
|
53
|
-
if response_env[:status] == 200
|
54
|
-
token_store.delete(token_store_key)
|
55
|
-
end
|
64
|
+
delete_token_store_key if response_env[:status] == 200
|
56
65
|
end
|
57
66
|
end
|
58
67
|
|
@@ -65,43 +74,33 @@ module FmRest
|
|
65
74
|
env.request_headers[HEADER_KEY] = "Bearer #{token}"
|
66
75
|
end
|
67
76
|
|
68
|
-
#
|
77
|
+
# Uses the token given in connection settings if available,
|
78
|
+
# otherwisek tries to get an existing token from the token store,
|
69
79
|
# otherwise requests one through basic auth,
|
70
80
|
# otherwise raises an exception.
|
71
81
|
#
|
72
82
|
def token
|
83
|
+
return @settings.token if @settings.token?
|
84
|
+
|
73
85
|
token = token_store.load(token_store_key)
|
74
86
|
return token if token
|
75
87
|
|
76
|
-
|
77
|
-
token_store.store(token_store_key, token)
|
78
|
-
return token
|
79
|
-
end
|
88
|
+
return nil unless @settings.autologin
|
80
89
|
|
81
|
-
|
82
|
-
|
90
|
+
token = V1.request_auth_token!(auth_connection)
|
91
|
+
token_store.store(token_store_key, token)
|
92
|
+
token
|
83
93
|
end
|
84
94
|
|
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
|
95
|
+
# The key to use to store a token, uses the format host:database:username
|
97
96
|
#
|
98
97
|
def token_store_key
|
99
98
|
@token_store_key ||=
|
100
99
|
begin
|
101
100
|
# Strip the host part to just the hostname (i.e. no scheme or port)
|
102
|
-
host = @
|
101
|
+
host = @settings.host!
|
103
102
|
host = URI(host).hostname if host =~ /\Ahttps?:\/\//
|
104
|
-
"#{host}:#{@
|
103
|
+
"#{host}:#{@settings.database!}:#{@settings.username!}"
|
105
104
|
end
|
106
105
|
end
|
107
106
|
|
@@ -111,31 +110,23 @@ module FmRest
|
|
111
110
|
if TOKEN_STORE_INTERFACE.all? { |method| token_store_option.respond_to?(method) }
|
112
111
|
token_store_option
|
113
112
|
elsif token_store_option.kind_of?(Class)
|
114
|
-
token_store_option.
|
113
|
+
if token_store_option.respond_to?(:instance)
|
114
|
+
token_store_option.instance
|
115
|
+
else
|
116
|
+
token_store_option.new
|
117
|
+
end
|
115
118
|
else
|
116
|
-
|
117
|
-
TokenStore::Memory.new
|
119
|
+
FmRest::TokenStore::Memory.new
|
118
120
|
end
|
119
121
|
end
|
120
122
|
end
|
121
123
|
|
122
124
|
def token_store_option
|
123
|
-
@
|
125
|
+
@settings.token_store || FmRest.token_store
|
124
126
|
end
|
125
127
|
|
126
128
|
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
|
129
|
+
@auth_connection ||= V1.auth_connection(@settings)
|
139
130
|
end
|
140
131
|
end
|
141
132
|
end
|