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.
@@ -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