fmrest-core 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 +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +523 -0
- data/lib/fmrest-core.rb +3 -0
- data/lib/fmrest-spyke.rb +3 -0
- data/lib/fmrest.rb +36 -0
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +30 -0
- data/lib/fmrest/string_date.rb +220 -0
- data/lib/fmrest/token_store.rb +12 -0
- data/lib/fmrest/token_store/active_record.rb +74 -0
- data/lib/fmrest/token_store/base.rb +25 -0
- data/lib/fmrest/token_store/memory.rb +26 -0
- data/lib/fmrest/token_store/moneta.rb +41 -0
- data/lib/fmrest/token_store/null.rb +20 -0
- data/lib/fmrest/token_store/redis.rb +45 -0
- data/lib/fmrest/v1.rb +23 -0
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +116 -0
- data/lib/fmrest/v1/container_fields.rb +114 -0
- data/lib/fmrest/v1/dates.rb +81 -0
- data/lib/fmrest/v1/paths.rb +42 -0
- data/lib/fmrest/v1/raise_errors.rb +57 -0
- data/lib/fmrest/v1/token_session.rb +133 -0
- data/lib/fmrest/v1/token_store/active_record.rb +13 -0
- data/lib/fmrest/v1/token_store/memory.rb +13 -0
- data/lib/fmrest/v1/type_coercer.rb +192 -0
- data/lib/fmrest/v1/utils.rb +113 -0
- data/lib/fmrest/version.rb +5 -0
- metadata +115 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module TokenStore
|
5
|
+
class Base
|
6
|
+
attr_reader :options
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def load(key)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(key, value)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(key)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/token_store/base"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module TokenStore
|
7
|
+
class Memory < Base
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
@tokens = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete(key)
|
14
|
+
@tokens.delete(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load(key)
|
18
|
+
@tokens[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def store(key, value)
|
22
|
+
@tokens[key] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/token_store/base"
|
4
|
+
require "moneta"
|
5
|
+
|
6
|
+
module FmRest
|
7
|
+
module TokenStore
|
8
|
+
class Moneta < Base
|
9
|
+
DEFAULT_ADAPTER = :Memory
|
10
|
+
DEFAULT_PREFIX = "fmrest-token:".freeze
|
11
|
+
|
12
|
+
attr_reader :moneta
|
13
|
+
|
14
|
+
# @param options [Hash]
|
15
|
+
# Options to pass to `Moneta.new`
|
16
|
+
# @option options [Symbol] :adapter (:Memory)
|
17
|
+
# The Moneta adapter to use
|
18
|
+
# @option options [String] :prefix (DEFAULT_PREFIX)
|
19
|
+
# The prefix to use for keys
|
20
|
+
def initialize(options = {})
|
21
|
+
options = options.dup
|
22
|
+
super(options)
|
23
|
+
adapter = options.delete(:adapter) || DEFAULT_ADAPTER
|
24
|
+
options[:prefix] ||= DEFAULT_PREFIX
|
25
|
+
@moneta = ::Moneta.new(adapter, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def load(key)
|
29
|
+
moneta[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(key)
|
33
|
+
moneta.delete(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def store(key, value)
|
37
|
+
moneta[key] = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module TokenStore
|
7
|
+
module Null < Base
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def delete(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(key, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/token_store/base"
|
4
|
+
require "redis" unless defined?(MockRedis)
|
5
|
+
|
6
|
+
module FmRest
|
7
|
+
module TokenStore
|
8
|
+
class Redis < Base
|
9
|
+
DEFAULT_PREFIX = "fmrest-token:".freeze
|
10
|
+
|
11
|
+
STORE_OPTIONS = [:redis, :prefix].freeze
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
super
|
15
|
+
@redis = @options[:redis] || ::Redis.new(options_for_redis)
|
16
|
+
@prefix = @options[:prefix] || DEFAULT_PREFIX
|
17
|
+
end
|
18
|
+
|
19
|
+
def load(key)
|
20
|
+
@redis.get(prefix_key(key))
|
21
|
+
end
|
22
|
+
|
23
|
+
def store(key, value)
|
24
|
+
@redis.set(prefix_key(key), value)
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
@redis.del(prefix_key(key))
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def options_for_redis
|
35
|
+
@options.dup.tap do |options|
|
36
|
+
STORE_OPTIONS.each { |opt| options.delete(opt) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def prefix_key(key)
|
41
|
+
"#{@prefix}#{key}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/fmrest/v1.rb
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
require "fmrest/v1/auth"
|
9
|
+
|
10
|
+
module FmRest
|
11
|
+
module V1
|
12
|
+
extend Connection
|
13
|
+
extend Paths
|
14
|
+
extend ContainerFields
|
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"
|
22
|
+
end
|
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
|
@@ -0,0 +1,116 @@
|
|
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"
|
9
|
+
DATABASES_PATH = "#{BASE_PATH}/databases"
|
10
|
+
|
11
|
+
AUTH_HEADERS = { "Content-Type" => "application/json" }.freeze
|
12
|
+
|
13
|
+
# Builds a complete DAPI Faraday connection with middleware already
|
14
|
+
# configured to handle authentication, JSON parsing, logging and DAPI
|
15
|
+
# error handling. A block can be optionally given for additional
|
16
|
+
# middleware configuration
|
17
|
+
#
|
18
|
+
# @option (see #base_connection)
|
19
|
+
# @return (see #base_connection)
|
20
|
+
def build_connection(settings = FmRest.default_connection_settings, &block)
|
21
|
+
settings = ConnectionSettings.wrap(settings)
|
22
|
+
|
23
|
+
base_connection(settings) do |conn|
|
24
|
+
conn.use RaiseErrors
|
25
|
+
conn.use TokenSession, settings
|
26
|
+
|
27
|
+
# The EncodeJson and Multipart middlewares only encode the request
|
28
|
+
# when the content type matches, so we can have them both here and
|
29
|
+
# still play nice with each other, we just need to set the content
|
30
|
+
# type to multipart/form-data when we want to submit a container
|
31
|
+
# field
|
32
|
+
conn.request :multipart
|
33
|
+
conn.request :json
|
34
|
+
|
35
|
+
# Allow overriding the default response middleware
|
36
|
+
if block_given?
|
37
|
+
yield conn, settings
|
38
|
+
else
|
39
|
+
conn.use TypeCoercer, settings
|
40
|
+
conn.response :json
|
41
|
+
end
|
42
|
+
|
43
|
+
if settings.log
|
44
|
+
conn.response :logger, nil, bodies: true, headers: true
|
45
|
+
end
|
46
|
+
|
47
|
+
conn.adapter Faraday.default_adapter
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Builds a Faraday connection to use for DAPI basic auth login
|
52
|
+
#
|
53
|
+
# @option (see #base_connection)
|
54
|
+
# @return (see #base_connection)
|
55
|
+
def auth_connection(settings = FmRest.default_connection_settings)
|
56
|
+
settings = ConnectionSettings.wrap(settings)
|
57
|
+
|
58
|
+
base_connection(settings, { headers: AUTH_HEADERS }) do |conn|
|
59
|
+
conn.use RaiseErrors
|
60
|
+
|
61
|
+
conn.basic_auth settings.username!, settings.password!
|
62
|
+
|
63
|
+
if settings.log
|
64
|
+
conn.response :logger, nil, bodies: true, headers: true
|
65
|
+
end
|
66
|
+
|
67
|
+
conn.response :json
|
68
|
+
conn.adapter Faraday.default_adapter
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builds a base Faraday connection with base URL constructed from
|
73
|
+
# connection settings and passes it the given block
|
74
|
+
#
|
75
|
+
# @option settings [String] :host The hostname for the FM server
|
76
|
+
# @option settings [String] :database The FM database name
|
77
|
+
# @option settings [String] :username The username for DAPI authentication
|
78
|
+
# @option settings [String] :account_name Alias of :username for
|
79
|
+
# compatibility with Rfm gem
|
80
|
+
# @option settings [String] :password The password for DAPI authentication
|
81
|
+
# @option settings [String] :ssl SSL settings to forward to the Faraday
|
82
|
+
# connection
|
83
|
+
# @option settings [String] :proxy Proxy options to forward to the Faraday
|
84
|
+
# connection
|
85
|
+
# @param faraday_options [Hash] additional options for Faraday object
|
86
|
+
# @return [Faraday] The new Faraday connection
|
87
|
+
def base_connection(settings = FmRest.default_connection_settings, faraday_options = nil, &block)
|
88
|
+
settings = ConnectionSettings.wrap(settings)
|
89
|
+
|
90
|
+
host = settings.host!
|
91
|
+
|
92
|
+
# Default to HTTPS
|
93
|
+
scheme = "https"
|
94
|
+
|
95
|
+
if host.match(/\Ahttps?:\/\//)
|
96
|
+
uri = URI(host)
|
97
|
+
host = uri.hostname
|
98
|
+
host += ":#{uri.port}" if uri.port != uri.default_port
|
99
|
+
scheme = uri.scheme
|
100
|
+
end
|
101
|
+
|
102
|
+
faraday_options = (faraday_options || {}).dup
|
103
|
+
faraday_options[:ssl] = settings.ssl if settings.ssl?
|
104
|
+
faraday_options[:proxy] = settings.proxy if settings.proxy?
|
105
|
+
|
106
|
+
database = V1.url_encode(settings.database!)
|
107
|
+
|
108
|
+
Faraday.new(
|
109
|
+
"#{scheme}://#{host}#{DATABASES_PATH}/#{database}/".freeze,
|
110
|
+
faraday_options,
|
111
|
+
&block
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,114 @@
|
|
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
|