fmrest 0.10.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +36 -0
  4. data/README.md +193 -761
  5. metadata +71 -98
  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 -91
  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,91 +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
- database = URI.encode_www_form_component(options.fetch(:database))
78
-
79
- Faraday.new(
80
- "#{scheme}://#{host}#{BASE_PATH}/#{database}/".freeze,
81
- faraday_options,
82
- &block
83
- )
84
- end
85
- end
86
- end
87
- end
88
-
89
- require "fmrest/v1/token_session"
90
- require "fmrest/v1/raise_errors"
91
- 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