fmrest 0.11.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +33 -0
  4. data/README.md +247 -847
  5. metadata +82 -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 -36
  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 -99
  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 -57
  48. data/lib/fmrest/v1/token_session.rb +0 -133
  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,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module V1
5
- # FM Data API response middleware for raising exceptions on API response
6
- # errors
7
- #
8
- # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
9
- #
10
- class RaiseErrors < Faraday::Response::Middleware
11
- # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
12
- ERROR_RANGES = {
13
- -1 => APIError::UnknownError,
14
- 100 => APIError::ResourceMissingError,
15
- 101 => APIError::RecordMissingError,
16
- 102..199 => APIError::ResourceMissingError,
17
- 200..299 => APIError::AccountError,
18
- 300..399 => APIError::LockError,
19
- 400 => APIError::ParameterError,
20
- 401 => APIError::NoMatchingRecordsError,
21
- 402..499 => APIError::ParameterError,
22
- 500..599 => APIError::ValidationError,
23
- 800..899 => APIError::SystemError,
24
- 952 => APIError::InvalidToken,
25
- 953 => APIError::MaximumDataAPICallsExceeded,
26
- 1200..1299 => APIError::ScriptError,
27
- 1400..1499 => APIError::ODBCError
28
- }.freeze
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,133 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fmrest/v1/connection"
4
-
5
- module FmRest
6
- module V1
7
- # FM Data API authentication middleware using the credentials strategy
8
- #
9
- class TokenSession < Faraday::Middleware
10
- class NoSessionTokenSet < FmRest::Error; end
11
-
12
- HEADER_KEY = "Authorization"
13
- TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze
14
- LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::BASE_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze
15
-
16
- # @param app [#call]
17
- # @param settings [FmRest::ConnectionSettings]
18
- def initialize(app, settings)
19
- super(app)
20
- @settings = settings
21
- end
22
-
23
- # Entry point for the middleware when sending a request
24
- #
25
- def call(env)
26
- return handle_logout(env) if is_logout_request?(env)
27
-
28
- set_auth_header(env)
29
-
30
- request_body = env[:body] # After failure env[:body] is set to the response body
31
-
32
- @app.call(env).on_complete do |response_env|
33
- if response_env[:status] == 401 # Unauthorized
34
- delete_token_store_key
35
-
36
- if @settings.autologin
37
- env[:body] = request_body
38
- set_auth_header(env)
39
- return @app.call(env)
40
- end
41
- end
42
- end
43
- end
44
-
45
- private
46
-
47
- def delete_token_store_key
48
- token_store.delete(token_store_key)
49
- # Sometimes we may want to pass the :token in settings manually, and
50
- # refrain from passing a :username. In that case the call to
51
- # #token_store_key above would fail as it tries to fetch :username, so
52
- # we purposely ignore that error.
53
- rescue FmRest::ConnectionSettings::MissingSetting
54
- end
55
-
56
- def handle_logout(env)
57
- token = @settings.token? ? @settings.token : token_store.load(token_store_key)
58
-
59
- raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token
60
-
61
- env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}")
62
-
63
- @app.call(env).on_complete do |response_env|
64
- delete_token_store_key if response_env[:status] == 200
65
- end
66
- end
67
-
68
- def is_logout_request?(env)
69
- return false unless env.method == :delete
70
- return env.url.path.match?(LOGOUT_PATH_MATCHER)
71
- end
72
-
73
- def set_auth_header(env)
74
- env.request_headers[HEADER_KEY] = "Bearer #{token}"
75
- end
76
-
77
- # Uses the token given in connection settings if available,
78
- # otherwisek tries to get an existing token from the token store,
79
- # otherwise requests one through basic auth,
80
- # otherwise raises an exception.
81
- #
82
- def token
83
- return @settings.token if @settings.token?
84
-
85
- token = token_store.load(token_store_key)
86
- return token if token
87
-
88
- return nil unless @settings.autologin
89
-
90
- token = V1.request_auth_token!(auth_connection)
91
- token_store.store(token_store_key, token)
92
- token
93
- end
94
-
95
- # The key to use to store a token, uses the format host:database:username
96
- #
97
- def token_store_key
98
- @token_store_key ||=
99
- begin
100
- # Strip the host part to just the hostname (i.e. no scheme or port)
101
- host = @settings.host!
102
- host = URI(host).hostname if host =~ /\Ahttps?:\/\//
103
- "#{host}:#{@settings.database!}:#{@settings.username!}"
104
- end
105
- end
106
-
107
- def token_store
108
- @token_store ||=
109
- begin
110
- if TOKEN_STORE_INTERFACE.all? { |method| token_store_option.respond_to?(method) }
111
- token_store_option
112
- elsif token_store_option.kind_of?(Class)
113
- if token_store_option.respond_to?(:instance)
114
- token_store_option.instance
115
- else
116
- token_store_option.new
117
- end
118
- else
119
- FmRest::TokenStore::Memory.new
120
- end
121
- end
122
- end
123
-
124
- def token_store_option
125
- @settings.token_store || FmRest.token_store
126
- end
127
-
128
- def auth_connection
129
- @auth_connection ||= V1.auth_connection(@settings)
130
- end
131
- end
132
- end
133
- 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