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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +194 -763
  5. metadata +70 -97
  6. data/.gitignore +0 -26
  7. data/.rspec +0 -3
  8. data/.travis.yml +0 -5
  9. data/Gemfile +0 -3
  10. data/Rakefile +0 -6
  11. data/fmrest.gemspec +0 -38
  12. data/lib/fmrest.rb +0 -29
  13. data/lib/fmrest/errors.rb +0 -28
  14. data/lib/fmrest/spyke.rb +0 -21
  15. data/lib/fmrest/spyke/base.rb +0 -23
  16. data/lib/fmrest/spyke/container_field.rb +0 -59
  17. data/lib/fmrest/spyke/model.rb +0 -36
  18. data/lib/fmrest/spyke/model/associations.rb +0 -82
  19. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  20. data/lib/fmrest/spyke/model/auth.rb +0 -35
  21. data/lib/fmrest/spyke/model/connection.rb +0 -74
  22. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  23. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  24. data/lib/fmrest/spyke/model/http.rb +0 -37
  25. data/lib/fmrest/spyke/model/orm.rb +0 -212
  26. data/lib/fmrest/spyke/model/serialization.rb +0 -91
  27. data/lib/fmrest/spyke/model/uri.rb +0 -30
  28. data/lib/fmrest/spyke/portal.rb +0 -55
  29. data/lib/fmrest/spyke/relation.rb +0 -359
  30. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  31. data/lib/fmrest/spyke/validation_error.rb +0 -25
  32. data/lib/fmrest/string_date.rb +0 -220
  33. data/lib/fmrest/token_store.rb +0 -6
  34. data/lib/fmrest/token_store/active_record.rb +0 -74
  35. data/lib/fmrest/token_store/base.rb +0 -25
  36. data/lib/fmrest/token_store/memory.rb +0 -26
  37. data/lib/fmrest/token_store/moneta.rb +0 -41
  38. data/lib/fmrest/token_store/redis.rb +0 -45
  39. data/lib/fmrest/v1.rb +0 -21
  40. data/lib/fmrest/v1/connection.rb +0 -89
  41. data/lib/fmrest/v1/container_fields.rb +0 -114
  42. data/lib/fmrest/v1/dates.rb +0 -81
  43. data/lib/fmrest/v1/paths.rb +0 -47
  44. data/lib/fmrest/v1/raise_errors.rb +0 -57
  45. data/lib/fmrest/v1/token_session.rb +0 -142
  46. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  47. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  48. data/lib/fmrest/v1/type_coercer.rb +0 -192
  49. data/lib/fmrest/v1/utils.rb +0 -95
  50. 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
@@ -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
@@ -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
@@ -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