fmrest 0.10.0 → 0.13.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/.yardopts +2 -0
- data/CHANGELOG.md +38 -0
- data/README.md +194 -763
- metadata +70 -97
- data/.gitignore +0 -26
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/Gemfile +0 -3
- data/Rakefile +0 -6
- data/fmrest.gemspec +0 -38
- data/lib/fmrest.rb +0 -29
- data/lib/fmrest/errors.rb +0 -28
- data/lib/fmrest/spyke.rb +0 -21
- data/lib/fmrest/spyke/base.rb +0 -23
- data/lib/fmrest/spyke/container_field.rb +0 -59
- data/lib/fmrest/spyke/model.rb +0 -36
- data/lib/fmrest/spyke/model/associations.rb +0 -82
- data/lib/fmrest/spyke/model/attributes.rb +0 -171
- data/lib/fmrest/spyke/model/auth.rb +0 -35
- data/lib/fmrest/spyke/model/connection.rb +0 -74
- data/lib/fmrest/spyke/model/container_fields.rb +0 -25
- data/lib/fmrest/spyke/model/global_fields.rb +0 -40
- data/lib/fmrest/spyke/model/http.rb +0 -37
- data/lib/fmrest/spyke/model/orm.rb +0 -212
- data/lib/fmrest/spyke/model/serialization.rb +0 -91
- data/lib/fmrest/spyke/model/uri.rb +0 -30
- data/lib/fmrest/spyke/portal.rb +0 -55
- data/lib/fmrest/spyke/relation.rb +0 -359
- data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
- data/lib/fmrest/spyke/validation_error.rb +0 -25
- data/lib/fmrest/string_date.rb +0 -220
- data/lib/fmrest/token_store.rb +0 -6
- data/lib/fmrest/token_store/active_record.rb +0 -74
- data/lib/fmrest/token_store/base.rb +0 -25
- data/lib/fmrest/token_store/memory.rb +0 -26
- data/lib/fmrest/token_store/moneta.rb +0 -41
- data/lib/fmrest/token_store/redis.rb +0 -45
- data/lib/fmrest/v1.rb +0 -21
- data/lib/fmrest/v1/connection.rb +0 -89
- data/lib/fmrest/v1/container_fields.rb +0 -114
- data/lib/fmrest/v1/dates.rb +0 -81
- data/lib/fmrest/v1/paths.rb +0 -47
- data/lib/fmrest/v1/raise_errors.rb +0 -57
- data/lib/fmrest/v1/token_session.rb +0 -142
- data/lib/fmrest/v1/token_store/active_record.rb +0 -13
- data/lib/fmrest/v1/token_store/memory.rb +0 -13
- data/lib/fmrest/v1/type_coercer.rb +0 -192
- data/lib/fmrest/v1/utils.rb +0 -95
- data/lib/fmrest/version.rb +0 -5
data/lib/fmrest/v1.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/v1/connection"
|
4
|
-
require "fmrest/v1/paths"
|
5
|
-
require "fmrest/v1/container_fields"
|
6
|
-
require "fmrest/v1/utils"
|
7
|
-
require "fmrest/v1/dates"
|
8
|
-
|
9
|
-
module FmRest
|
10
|
-
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
|
-
extend Connection
|
16
|
-
extend Paths
|
17
|
-
extend ContainerFields
|
18
|
-
extend Utils
|
19
|
-
extend Dates
|
20
|
-
end
|
21
|
-
end
|
data/lib/fmrest/v1/connection.rb
DELETED
@@ -1,89 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "uri"
|
4
|
-
|
5
|
-
module FmRest
|
6
|
-
module V1
|
7
|
-
module Connection
|
8
|
-
BASE_PATH = "/fmi/data/v1/databases"
|
9
|
-
|
10
|
-
# Builds a complete DAPI Faraday connection with middleware already
|
11
|
-
# configured to handle authentication, JSON parsing, logging and DAPI
|
12
|
-
# error handling. A block can be optionally given for additional
|
13
|
-
# middleware configuration
|
14
|
-
#
|
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
|
-
# @option (see #base_connection)
|
20
|
-
# @return (see #base_connection)
|
21
|
-
def build_connection(options = FmRest.default_connection_settings, &block)
|
22
|
-
base_connection(options) do |conn|
|
23
|
-
conn.use RaiseErrors
|
24
|
-
conn.use TokenSession, options
|
25
|
-
|
26
|
-
# The EncodeJson and Multipart middlewares only encode the request
|
27
|
-
# when the content type matches, so we can have them both here and
|
28
|
-
# still play nice with each other, we just need to set the content
|
29
|
-
# type to multipart/form-data when we want to submit a container
|
30
|
-
# field
|
31
|
-
conn.request :multipart
|
32
|
-
conn.request :json
|
33
|
-
|
34
|
-
# Allow overriding the default response middleware
|
35
|
-
if block_given?
|
36
|
-
yield conn, options
|
37
|
-
else
|
38
|
-
conn.use TypeCoercer, options
|
39
|
-
conn.response :json
|
40
|
-
end
|
41
|
-
|
42
|
-
if options[: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 base Faraday connection with base URL constructed from
|
51
|
-
# connection options and passes it the given block
|
52
|
-
#
|
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
|
56
|
-
# connection
|
57
|
-
# @option options [String] :proxy Proxy options to forward to the Faraday
|
58
|
-
# connection
|
59
|
-
# @return [Faraday] The new Faraday connection
|
60
|
-
def base_connection(options = FmRest.default_connection_settings, &block)
|
61
|
-
host = options.fetch(:host)
|
62
|
-
|
63
|
-
# Default to HTTPS
|
64
|
-
scheme = "https"
|
65
|
-
|
66
|
-
if host.match(/\Ahttps?:\/\//)
|
67
|
-
uri = URI(host)
|
68
|
-
host = uri.hostname
|
69
|
-
host += ":#{uri.port}" if uri.port != uri.default_port
|
70
|
-
scheme = uri.scheme
|
71
|
-
end
|
72
|
-
|
73
|
-
faraday_options = {}
|
74
|
-
faraday_options[:ssl] = options[:ssl] if options.key?(:ssl)
|
75
|
-
faraday_options[:proxy] = options[:proxy] if options.key?(:proxy)
|
76
|
-
|
77
|
-
Faraday.new(
|
78
|
-
"#{scheme}://#{host}#{BASE_PATH}/#{URI.escape(options.fetch(:database))}/".freeze,
|
79
|
-
faraday_options,
|
80
|
-
&block
|
81
|
-
)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
require "fmrest/v1/token_session"
|
88
|
-
require "fmrest/v1/raise_errors"
|
89
|
-
require "fmrest/v1/type_coercer"
|
@@ -1,114 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module V1
|
5
|
-
module ContainerFields
|
6
|
-
DEFAULT_UPLOAD_CONTENT_TYPE = "application/octet-stream".freeze
|
7
|
-
|
8
|
-
# Given a container field URL it tries to fetch it and returns an IO
|
9
|
-
# object with its body content (see Ruby's OpenURI for how the IO object
|
10
|
-
# is extended with useful HTTP response information).
|
11
|
-
#
|
12
|
-
# This method uses OpenURI instead of Faraday for fetching the actual
|
13
|
-
# container file.
|
14
|
-
#
|
15
|
-
# @raise [FmRest::ContainerFieldError] if any step fails
|
16
|
-
# @param container_field_url [String] The URL to the container to
|
17
|
-
# download
|
18
|
-
# @param base_connection [Faraday::Connection] An optional Faraday
|
19
|
-
# connection to use as base settings for the container requests, useful
|
20
|
-
# if you need to set SSL or proxy settings. If given, this connection
|
21
|
-
# will not be used directly, but rather a new one with copied SSL and
|
22
|
-
# proxy options. If omitted, `FmRest.default_connection_settings`'s
|
23
|
-
# `:ssl` and `:proxy` options will be used instead (if available)
|
24
|
-
# @return [IO] The contents of the container
|
25
|
-
def fetch_container_data(container_field_url, base_connection = nil)
|
26
|
-
require "open-uri"
|
27
|
-
|
28
|
-
begin
|
29
|
-
url = URI(container_field_url)
|
30
|
-
rescue ::URI::InvalidURIError
|
31
|
-
raise FmRest::ContainerFieldError, "Invalid container field URL `#{container_field_url}'"
|
32
|
-
end
|
33
|
-
|
34
|
-
# Make sure we don't try to open anything on the file:/ URI scheme
|
35
|
-
unless url.scheme.match(/\Ahttps?\Z/)
|
36
|
-
raise FmRest::ContainerFieldError, "Container URL is not HTTP (#{container_field_url})"
|
37
|
-
end
|
38
|
-
|
39
|
-
ssl_options = base_connection && base_connection.ssl && base_connection.ssl.to_hash
|
40
|
-
proxy_options = base_connection && base_connection.proxy && base_connection.proxy.to_hash
|
41
|
-
|
42
|
-
conn =
|
43
|
-
Faraday.new(nil,
|
44
|
-
ssl: ssl_options || FmRest.default_connection_settings[:ssl],
|
45
|
-
proxy: proxy_options || FmRest.default_connection_settings[:proxy]
|
46
|
-
)
|
47
|
-
|
48
|
-
# Requesting the container URL with no cookie set will respond with a
|
49
|
-
# redirect and a session cookie
|
50
|
-
cookie_response = conn.get url
|
51
|
-
|
52
|
-
unless cookie = cookie_response.headers["Set-Cookie"]
|
53
|
-
raise FmRest::ContainerFieldError, "Container field's initial request didn't return a session cookie, the URL may be stale (try downloading it again immediately after retrieving the record)"
|
54
|
-
end
|
55
|
-
|
56
|
-
# Now request the URL again with the proper session cookie using
|
57
|
-
# OpenURI, which wraps the response in an IO object which also responds
|
58
|
-
# to #content_type
|
59
|
-
url.open(faraday_connection_to_openuri_options(conn).merge("Cookie" => cookie))
|
60
|
-
end
|
61
|
-
|
62
|
-
# Handles the core logic of uploading a file into a container field
|
63
|
-
#
|
64
|
-
# @param connection [Faraday::Connection] the Faraday connection to use
|
65
|
-
# @param container_path [String] the path to the container
|
66
|
-
# @param filename_or_io [String, IO] a path to the file to upload or an
|
67
|
-
# IO object
|
68
|
-
# @param options [Hash]
|
69
|
-
# @option options [String] :content_type (DEFAULT_UPLOAD_CONTENT_TYPE)
|
70
|
-
# The content type for the uploaded file
|
71
|
-
# @option options [String] :filename The filename to use for the uploaded
|
72
|
-
# file, defaults to `filename_or_io.original_filename` if available
|
73
|
-
def upload_container_data(connection, container_path, filename_or_io, options = {})
|
74
|
-
content_type = options[:content_type] || DEFAULT_UPLOAD_CONTENT_TYPE
|
75
|
-
|
76
|
-
connection.post do |request|
|
77
|
-
request.url container_path
|
78
|
-
request.headers['Content-Type'] = ::Faraday::Request::Multipart.mime_type
|
79
|
-
|
80
|
-
filename = options[:filename] || filename_or_io.try(:original_filename)
|
81
|
-
|
82
|
-
request.body = { upload: Faraday::UploadIO.new(filename_or_io, content_type, filename) }
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
# Copies a Faraday::Connection's relevant options to
|
89
|
-
# OpenURI::OpenRead#open format
|
90
|
-
#
|
91
|
-
def faraday_connection_to_openuri_options(conn)
|
92
|
-
openuri_opts = {}
|
93
|
-
|
94
|
-
if !conn.ssl.empty?
|
95
|
-
openuri_opts[:ssl_verify_mode] =
|
96
|
-
conn.ssl.fetch(:verify, true) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
97
|
-
|
98
|
-
openuri_opts[:ssl_ca_cert] = conn.ssl.cert_store if conn.ssl.cert_store
|
99
|
-
end
|
100
|
-
|
101
|
-
if conn.proxy && !conn.proxy.empty?
|
102
|
-
if conn.proxy.user && conn.proxy.password
|
103
|
-
openuri_opts[:proxy_http_basic_authentication] =
|
104
|
-
[conn.proxy.uri.tap { |u| u.userinfo = ""}, conn.proxy.user, conn.proxy.password]
|
105
|
-
else
|
106
|
-
openuri_opts[:proxy] = conn.proxy.uri
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
openuri_opts
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
data/lib/fmrest/v1/dates.rb
DELETED
@@ -1,81 +0,0 @@
|
|
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
|
data/lib/fmrest/v1/paths.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module V1
|
5
|
-
module Paths
|
6
|
-
def session_path(token = nil)
|
7
|
-
url = "sessions"
|
8
|
-
url += "/#{token}" if token
|
9
|
-
url
|
10
|
-
end
|
11
|
-
|
12
|
-
def record_path(layout, id = nil)
|
13
|
-
url = "layouts/#{url_encode(layout)}/records"
|
14
|
-
url += "/#{id}" if id
|
15
|
-
url
|
16
|
-
end
|
17
|
-
|
18
|
-
def container_field_path(layout, id, field_name, field_repetition = 1)
|
19
|
-
url = record_path(layout, id)
|
20
|
-
url += "/containers/#{url_encode(field_name)}"
|
21
|
-
url += "/#{field_repetition}" if field_repetition
|
22
|
-
url
|
23
|
-
end
|
24
|
-
|
25
|
-
def find_path(layout)
|
26
|
-
"layouts/#{url_encode(layout)}/_find"
|
27
|
-
end
|
28
|
-
|
29
|
-
def script_path(layout, script)
|
30
|
-
"layouts/#{url_encode(layout)}/script/#{url_encode(script)}"
|
31
|
-
end
|
32
|
-
|
33
|
-
def globals_path
|
34
|
-
"globals"
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
# Borrowed from ERB::Util
|
40
|
-
def url_encode(s)
|
41
|
-
s.to_s.b.gsub(/[^a-zA-Z0-9_\-.]/n) { |m|
|
42
|
-
sprintf("%%%02X", m.unpack("C")[0])
|
43
|
-
}
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/errors"
|
4
|
-
|
5
|
-
module FmRest
|
6
|
-
module V1
|
7
|
-
# FM Data API response middleware for raising exceptions on API response
|
8
|
-
# errors
|
9
|
-
#
|
10
|
-
# https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
|
11
|
-
#
|
12
|
-
class RaiseErrors < Faraday::Response::Middleware
|
13
|
-
# https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
|
14
|
-
ERROR_RANGES = {
|
15
|
-
-1 => APIError::UnknownError,
|
16
|
-
100 => APIError::ResourceMissingError,
|
17
|
-
101 => APIError::RecordMissingError,
|
18
|
-
102..199 => APIError::ResourceMissingError,
|
19
|
-
200..299 => APIError::AccountError,
|
20
|
-
300..399 => APIError::LockError,
|
21
|
-
400 => APIError::ParameterError,
|
22
|
-
401 => APIError::NoMatchingRecordsError,
|
23
|
-
402..499 => APIError::ParameterError,
|
24
|
-
500..599 => APIError::ValidationError,
|
25
|
-
800..899 => APIError::SystemError,
|
26
|
-
1200..1299 => APIError::ScriptError,
|
27
|
-
1400..1499 => APIError::ODBCError
|
28
|
-
}
|
29
|
-
|
30
|
-
def on_complete(env)
|
31
|
-
# Sniff for either straight JSON parsing or Spyke's format
|
32
|
-
if env.body[:metadata] && env.body[:metadata][:messages]
|
33
|
-
check_errors(env.body[:metadata][:messages])
|
34
|
-
elsif env.body["messages"]
|
35
|
-
check_errors(env.body["messages"])
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def check_errors(messages)
|
42
|
-
messages.each do |message|
|
43
|
-
error_code = (message["code"] || message[:code]).to_i
|
44
|
-
|
45
|
-
# Code 0 means "No Error"
|
46
|
-
next if error_code.zero?
|
47
|
-
|
48
|
-
error_message = message["message"] || message[:message]
|
49
|
-
|
50
|
-
*, exception_class = ERROR_RANGES.find { |k, v| k === error_code }
|
51
|
-
|
52
|
-
raise (exception_class || APIError).new(error_code, error_message)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,142 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/v1/connection"
|
4
|
-
require "fmrest/errors"
|
5
|
-
|
6
|
-
module FmRest
|
7
|
-
module V1
|
8
|
-
# FM Data API authentication middleware using the credentials strategy
|
9
|
-
#
|
10
|
-
class TokenSession < Faraday::Middleware
|
11
|
-
class NoSessionTokenSet < FmRest::Error; end
|
12
|
-
|
13
|
-
HEADER_KEY = "Authorization".freeze
|
14
|
-
TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze
|
15
|
-
LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::BASE_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze
|
16
|
-
|
17
|
-
# @param app [#call]
|
18
|
-
# @param options [Hash]
|
19
|
-
def initialize(app, options = FmRest.default_connection_settings)
|
20
|
-
super(app)
|
21
|
-
@options = options
|
22
|
-
end
|
23
|
-
|
24
|
-
# Entry point for the middleware when sending a request
|
25
|
-
#
|
26
|
-
def call(env)
|
27
|
-
return handle_logout(env) if is_logout_request?(env)
|
28
|
-
|
29
|
-
set_auth_header(env)
|
30
|
-
|
31
|
-
request_body = env[:body] # After failure env[:body] is set to the response body
|
32
|
-
|
33
|
-
@app.call(env).on_complete do |response_env|
|
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)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def handle_logout(env)
|
46
|
-
token = token_store.load(token_store_key)
|
47
|
-
|
48
|
-
raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token
|
49
|
-
|
50
|
-
env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}")
|
51
|
-
|
52
|
-
@app.call(env).on_complete do |response_env|
|
53
|
-
if response_env[:status] == 200
|
54
|
-
token_store.delete(token_store_key)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def is_logout_request?(env)
|
60
|
-
return false unless env.method == :delete
|
61
|
-
return env.url.path.match?(LOGOUT_PATH_MATCHER)
|
62
|
-
end
|
63
|
-
|
64
|
-
def set_auth_header(env)
|
65
|
-
env.request_headers[HEADER_KEY] = "Bearer #{token}"
|
66
|
-
end
|
67
|
-
|
68
|
-
# Tries to get an existing token from the token store,
|
69
|
-
# otherwise requests one through basic auth,
|
70
|
-
# otherwise raises an exception.
|
71
|
-
#
|
72
|
-
def token
|
73
|
-
token = token_store.load(token_store_key)
|
74
|
-
return token if token
|
75
|
-
|
76
|
-
if token = request_token
|
77
|
-
token_store.store(token_store_key, token)
|
78
|
-
return token
|
79
|
-
end
|
80
|
-
|
81
|
-
# TODO: Make this a custom exception class
|
82
|
-
raise "Filemaker auth failed"
|
83
|
-
end
|
84
|
-
|
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
|
97
|
-
#
|
98
|
-
def token_store_key
|
99
|
-
@token_store_key ||=
|
100
|
-
begin
|
101
|
-
# Strip the host part to just the hostname (i.e. no scheme or port)
|
102
|
-
host = @options.fetch(:host)
|
103
|
-
host = URI(host).hostname if host =~ /\Ahttps?:\/\//
|
104
|
-
"#{host}:#{@options.fetch(:database)}"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def token_store
|
109
|
-
@token_store ||=
|
110
|
-
begin
|
111
|
-
if TOKEN_STORE_INTERFACE.all? { |method| token_store_option.respond_to?(method) }
|
112
|
-
token_store_option
|
113
|
-
elsif token_store_option.kind_of?(Class)
|
114
|
-
token_store_option.new
|
115
|
-
else
|
116
|
-
require "fmrest/token_store/memory"
|
117
|
-
TokenStore::Memory.new
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def token_store_option
|
123
|
-
@options[:token_store] || FmRest.token_store
|
124
|
-
end
|
125
|
-
|
126
|
-
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
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|