fmrest 0.11.0 → 0.14.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +32 -0
  4. data/README.md +228 -844
  5. metadata +71 -101
  6. data/.github/workflows/ci.yml +0 -33
  7. data/.gitignore +0 -26
  8. data/.rspec +0 -3
  9. data/.travis.yml +0 -5
  10. data/Gemfile +0 -3
  11. data/Rakefile +0 -6
  12. data/fmrest.gemspec +0 -38
  13. data/lib/fmrest.rb +0 -34
  14. data/lib/fmrest/connection_settings.rb +0 -124
  15. data/lib/fmrest/errors.rb +0 -30
  16. data/lib/fmrest/spyke.rb +0 -21
  17. data/lib/fmrest/spyke/base.rb +0 -23
  18. data/lib/fmrest/spyke/container_field.rb +0 -59
  19. data/lib/fmrest/spyke/model.rb +0 -36
  20. data/lib/fmrest/spyke/model/associations.rb +0 -82
  21. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  22. data/lib/fmrest/spyke/model/auth.rb +0 -43
  23. data/lib/fmrest/spyke/model/connection.rb +0 -135
  24. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  25. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  26. data/lib/fmrest/spyke/model/http.rb +0 -37
  27. data/lib/fmrest/spyke/model/orm.rb +0 -212
  28. data/lib/fmrest/spyke/model/serialization.rb +0 -91
  29. data/lib/fmrest/spyke/model/uri.rb +0 -30
  30. data/lib/fmrest/spyke/portal.rb +0 -55
  31. data/lib/fmrest/spyke/relation.rb +0 -359
  32. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  33. data/lib/fmrest/spyke/validation_error.rb +0 -25
  34. data/lib/fmrest/string_date.rb +0 -220
  35. data/lib/fmrest/token_store.rb +0 -12
  36. data/lib/fmrest/token_store/active_record.rb +0 -74
  37. data/lib/fmrest/token_store/base.rb +0 -25
  38. data/lib/fmrest/token_store/memory.rb +0 -26
  39. data/lib/fmrest/token_store/moneta.rb +0 -41
  40. data/lib/fmrest/token_store/redis.rb +0 -45
  41. data/lib/fmrest/v1.rb +0 -23
  42. data/lib/fmrest/v1/auth.rb +0 -30
  43. data/lib/fmrest/v1/connection.rb +0 -115
  44. data/lib/fmrest/v1/container_fields.rb +0 -114
  45. data/lib/fmrest/v1/dates.rb +0 -81
  46. data/lib/fmrest/v1/paths.rb +0 -47
  47. data/lib/fmrest/v1/raise_errors.rb +0 -59
  48. data/lib/fmrest/v1/token_session.rb +0 -134
  49. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  50. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  51. data/lib/fmrest/v1/type_coercer.rb +0 -192
  52. data/lib/fmrest/v1/utils.rb +0 -94
  53. data/lib/fmrest/version.rb +0 -5
@@ -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,59 +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
- 952 => APIError::InvalidToken,
27
- 953 => APIError::MaximumDataAPICallsExceeded,
28
- 1200..1299 => APIError::ScriptError,
29
- 1400..1499 => APIError::ODBCError
30
- }.freeze
31
-
32
- def on_complete(env)
33
- # Sniff for either straight JSON parsing or Spyke's format
34
- if env.body[:metadata] && env.body[:metadata][:messages]
35
- check_errors(env.body[:metadata][:messages])
36
- elsif env.body["messages"]
37
- check_errors(env.body["messages"])
38
- end
39
- end
40
-
41
- private
42
-
43
- def check_errors(messages)
44
- messages.each do |message|
45
- error_code = (message["code"] || message[:code]).to_i
46
-
47
- # Code 0 means "No Error"
48
- next if error_code.zero?
49
-
50
- error_message = message["message"] || message[:message]
51
-
52
- *, exception_class = ERROR_RANGES.find { |k, v| k === error_code }
53
-
54
- raise (exception_class || APIError).new(error_code, error_message)
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,134 +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"
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 settings [FmRest::ConnectionSettings]
19
- def initialize(app, settings)
20
- super(app)
21
- @settings = settings
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
- delete_token_store_key
36
-
37
- if @settings.autologin
38
- env[:body] = request_body
39
- set_auth_header(env)
40
- return @app.call(env)
41
- end
42
- end
43
- end
44
- end
45
-
46
- private
47
-
48
- def delete_token_store_key
49
- token_store.delete(token_store_key)
50
- # Sometimes we may want to pass the :token in settings manually, and
51
- # refrain from passing a :username. In that case the call to
52
- # #token_store_key above would fail as it tries to fetch :username, so
53
- # we purposely ignore that error.
54
- rescue FmRest::ConnectionSettings::MissingSetting
55
- end
56
-
57
- def handle_logout(env)
58
- token = @settings.token? ? @settings.token : token_store.load(token_store_key)
59
-
60
- raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token
61
-
62
- env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}")
63
-
64
- @app.call(env).on_complete do |response_env|
65
- delete_token_store_key if response_env[:status] == 200
66
- end
67
- end
68
-
69
- def is_logout_request?(env)
70
- return false unless env.method == :delete
71
- return env.url.path.match?(LOGOUT_PATH_MATCHER)
72
- end
73
-
74
- def set_auth_header(env)
75
- env.request_headers[HEADER_KEY] = "Bearer #{token}"
76
- end
77
-
78
- # Uses the token given in connection settings if available,
79
- # otherwisek tries to get an existing token from the token store,
80
- # otherwise requests one through basic auth,
81
- # otherwise raises an exception.
82
- #
83
- def token
84
- return @settings.token if @settings.token?
85
-
86
- token = token_store.load(token_store_key)
87
- return token if token
88
-
89
- return nil unless @settings.autologin
90
-
91
- token = V1.request_auth_token!(auth_connection)
92
- token_store.store(token_store_key, token)
93
- token
94
- end
95
-
96
- # The key to use to store a token, uses the format host:database:username
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 = @settings.host!
103
- host = URI(host).hostname if host =~ /\Ahttps?:\/\//
104
- "#{host}:#{@settings.database!}:#{@settings.username!}"
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
- if token_store_option.respond_to?(:instance)
115
- token_store_option.instance
116
- else
117
- token_store_option.new
118
- end
119
- else
120
- FmRest::TokenStore::Memory.new
121
- end
122
- end
123
- end
124
-
125
- def token_store_option
126
- @settings.token_store || FmRest.token_store
127
- end
128
-
129
- def auth_connection
130
- @auth_connection ||= V1.auth_connection(@settings)
131
- end
132
- end
133
- end
134
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- warn "FmRest::V1::TokenStore::ActiveRecord is deprecated, use FmRest::TokenStore::ActiveRecord instead"
4
-
5
- require "fmrest/token_store/active_record"
6
-
7
- module FmRest
8
- module V1
9
- module TokenStore
10
- ActiveRecord = ::FmRest::TokenStore::ActiveRecord
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- warn "FmRest::V1::TokenStore::Memory is deprecated, use FmRest::TokenStore::Memory instead"
4
-
5
- require "fmrest/token_store/memory"
6
-
7
- module FmRest
8
- module V1
9
- module TokenStore
10
- Memory = ::FmRest::TokenStore::Memory
11
- end
12
- end
13
- end
@@ -1,192 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fmrest/string_date"
4
-
5
- module FmRest
6
- module V1
7
- class TypeCoercer < Faraday::Response::Middleware
8
- # We use this date to represent a FileMaker time for consistency with
9
- # ginjo-rfm
10
- JULIAN_ZERO_DAY = "-4712/1/1"
11
-
12
- COERCE_HYBRID = [:hybrid, "hybrid", true].freeze
13
- COERCE_FULL = [:full, "full"].freeze
14
-
15
- # @param app [#call]
16
- # @param settings [FmRest::ConnectionSettings]
17
- def initialize(app, settings)
18
- super(app)
19
- @settings = settings
20
- end
21
-
22
- def on_complete(env)
23
- return unless enabled?
24
- return unless env.body.kind_of?(Hash)
25
-
26
- data = env.body.dig("response", "data") || env.body.dig(:response, :data)
27
-
28
- return unless data
29
-
30
- data.each do |record|
31
- field_data = record["fieldData"] || record[:fieldData]
32
- portal_data = record["portalData"] || record[:portalData]
33
-
34
- coerce_fields(field_data)
35
-
36
- portal_data.try(:each_value) do |portal_records|
37
- portal_records.each do |pr|
38
- coerce_fields(pr)
39
- end
40
- end
41
- end
42
- end
43
-
44
- private
45
-
46
- def coerce_fields(hash)
47
- hash.each do |k, v|
48
- next unless v.is_a?(String)
49
- next if k == "recordId" || k == :recordId || k == "modId" || k == :modId
50
-
51
- if quick_check_timestamp(v)
52
- begin
53
- hash[k] = coerce_timestamp(v)
54
- next
55
- rescue ArgumentError
56
- end
57
- end
58
-
59
- if quick_check_date(v)
60
- begin
61
- hash[k] = date_class.strptime(v, date_strptime_format)
62
- next
63
- rescue ArgumentError
64
- end
65
- end
66
-
67
- if quick_check_time(v)
68
- begin
69
- hash[k] = datetime_class.strptime("#{JULIAN_ZERO_DAY} #{v}", time_strptime_format)
70
- next
71
- rescue ArgumentError
72
- end
73
- end
74
- end
75
- end
76
-
77
- def coerce_timestamp(str)
78
- str_timestamp = DateTime.strptime(str, datetime_strptime_format)
79
-
80
- if local_timezone?
81
- # Change the DateTime to the local timezone, keeping the same
82
- # time and just modifying the timezone
83
- offset = FmRest::V1.local_offset_for_datetime(str_timestamp)
84
- str_timestamp = str_timestamp.new_offset(offset) - offset
85
- end
86
-
87
- if datetime_class == StringDateTime
88
- str_timestamp = StringDateTime.new(str, str_timestamp)
89
- end
90
-
91
- str_timestamp
92
- end
93
-
94
- def date_class
95
- @date_class ||=
96
- case coerce_dates
97
- when *COERCE_HYBRID
98
- StringDate
99
- when *COERCE_FULL
100
- Date
101
- end
102
- end
103
-
104
- def datetime_class
105
- @datetime_class ||=
106
- case coerce_dates
107
- when *COERCE_HYBRID
108
- StringDateTime
109
- when *COERCE_FULL
110
- DateTime
111
- end
112
- end
113
-
114
- def date_fm_format
115
- @settings.date_format
116
- end
117
-
118
- def timestamp_fm_format
119
- @settings.timestamp_format
120
- end
121
-
122
- def time_fm_format
123
- @settings.time_format
124
- end
125
-
126
- def date_strptime_format
127
- FmRest::V1.fm_date_to_strptime_format(date_fm_format)
128
- end
129
-
130
- def datetime_strptime_format
131
- FmRest::V1.fm_date_to_strptime_format(timestamp_fm_format)
132
- end
133
-
134
- def time_strptime_format
135
- @time_strptime_format ||=
136
- "%Y/%m/%d " + FmRest::V1.fm_date_to_strptime_format(time_fm_format)
137
- end
138
-
139
- # We use a string length test, followed by regexp match test to try to
140
- # identify date fields. Benchmarking shows this should be between 1 and 3
141
- # orders of magnitude faster for fails (i.e. non-dates) than just using
142
- # Date.strptime.
143
- #
144
- # user system total real
145
- # strptime: 0.268496 0.000962 0.269458 ( 0.270865)
146
- # re=~: 0.024872 0.000070 0.024942 ( 0.025057)
147
- # re.match?: 0.019745 0.000095 0.019840 ( 0.020058)
148
- # strptime fail: 0.141309 0.000354 0.141663 ( 0.142266)
149
- # re=~ fail: 0.031637 0.000095 0.031732 ( 0.031872)
150
- # re.match? fail: 0.011249 0.000056 0.011305 ( 0.011375)
151
- # length fail: 0.007177 0.000024 0.007201 ( 0.007222)
152
- #
153
- # NOTE: The faster Regexp#match? was introduced in Ruby 2.4.0, so we
154
- # can't really rely on it being available
155
- if //.respond_to?(:match?)
156
- def quick_check_timestamp(v)
157
- v.length == timestamp_fm_format.length && FmRest::V1::fm_date_to_regexp(timestamp_fm_format).match?(v)
158
- end
159
-
160
- def quick_check_date(v)
161
- v.length == date_fm_format.length && FmRest::V1::fm_date_to_regexp(date_fm_format).match?(v)
162
- end
163
-
164
- def quick_check_time(v)
165
- v.length == time_fm_format.length && FmRest::V1::fm_date_to_regexp(time_fm_format).match?(v)
166
- end
167
- else
168
- def quick_check_timestamp(v)
169
- v.length == timestamp_fm_format.length && FmRest::V1::fm_date_to_regexp(timestamp_fm_format) =~ v
170
- end
171
-
172
- def quick_check_date(v)
173
- v.length == date_fm_format.length && FmRest::V1::fm_date_to_regexp(date_fm_format) =~ v
174
- end
175
-
176
- def quick_check_time(v)
177
- v.length == time_fm_format.length && FmRest::V1::fm_date_to_regexp(time_fm_format) =~ v
178
- end
179
- end
180
-
181
- def local_timezone?
182
- @local_timezone ||= @settings.timezone.try(:to_sym) == :local
183
- end
184
-
185
- def coerce_dates
186
- @settings.coerce_dates
187
- end
188
-
189
- alias_method :enabled?, :coerce_dates
190
- end
191
- end
192
- end