usps-imis-api 0.11.32 → 1.0.0.pre.rc.1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +57 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +88 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov +8 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +95 -0
  10. data/Rakefile +12 -0
  11. data/Readme.md +191 -19
  12. data/bin/console +21 -0
  13. data/bin/setup +8 -0
  14. data/lib/ext/hash.rb +10 -0
  15. data/lib/usps/imis/api.rb +138 -211
  16. data/lib/usps/imis/config.rb +10 -68
  17. data/lib/usps/imis/error/api.rb +26 -0
  18. data/lib/usps/imis/error/mapper.rb +9 -0
  19. data/lib/usps/imis/error/response.rb +75 -0
  20. data/lib/usps/imis/mapper.rb +21 -90
  21. data/lib/usps/imis/panel/base_panel.rb +42 -0
  22. data/lib/usps/imis/panel/education.rb +111 -0
  23. data/lib/usps/imis/panel/vsc.rb +109 -0
  24. data/lib/usps/imis/version.rb +1 -1
  25. data/lib/usps/imis.rb +17 -33
  26. data/spec/lib/usps/imis/api_spec.rb +143 -0
  27. data/spec/lib/usps/imis/config_spec.rb +33 -0
  28. data/spec/lib/usps/imis/error/api_spec.rb +17 -0
  29. data/spec/lib/usps/imis/error/response_spec.rb +107 -0
  30. data/spec/lib/usps/imis/mapper_spec.rb +31 -0
  31. data/spec/lib/usps/imis/panel/base_panel_spec.rb +32 -0
  32. data/spec/lib/usps/imis/panel/education_spec.rb +55 -0
  33. data/spec/lib/usps/imis/panel/vsc_spec.rb +38 -0
  34. data/spec/lib/usps/imis_spec.rb +11 -0
  35. data/spec/spec_helper.rb +35 -0
  36. data/usps-imis-api.gemspec +18 -0
  37. metadata +33 -98
  38. data/bin/imis +0 -8
  39. data/lib/usps/imis/base_data.rb +0 -68
  40. data/lib/usps/imis/blank_object.rb +0 -62
  41. data/lib/usps/imis/business_object.rb +0 -230
  42. data/lib/usps/imis/command_line/interface.rb +0 -165
  43. data/lib/usps/imis/command_line/options_parser.rb +0 -139
  44. data/lib/usps/imis/command_line/performers.rb +0 -80
  45. data/lib/usps/imis/command_line.rb +0 -15
  46. data/lib/usps/imis/data.rb +0 -60
  47. data/lib/usps/imis/error.rb +0 -55
  48. data/lib/usps/imis/errors/api_error.rb +0 -11
  49. data/lib/usps/imis/errors/command_line_error.rb +0 -11
  50. data/lib/usps/imis/errors/config_error.rb +0 -11
  51. data/lib/usps/imis/errors/locked_id_error.rb +0 -15
  52. data/lib/usps/imis/errors/mapper_error.rb +0 -29
  53. data/lib/usps/imis/errors/missing_id_error.rb +0 -15
  54. data/lib/usps/imis/errors/not_found_error.rb +0 -11
  55. data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
  56. data/lib/usps/imis/errors/response_error.rb +0 -104
  57. data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
  58. data/lib/usps/imis/logger.rb +0 -19
  59. data/lib/usps/imis/logger_formatter.rb +0 -32
  60. data/lib/usps/imis/logger_helpers.rb +0 -20
  61. data/lib/usps/imis/mocks/business_object.rb +0 -47
  62. data/lib/usps/imis/mocks.rb +0 -11
  63. data/lib/usps/imis/panels/base_panel.rb +0 -125
  64. data/lib/usps/imis/panels/education.rb +0 -29
  65. data/lib/usps/imis/panels/vsc.rb +0 -28
  66. data/lib/usps/imis/panels.rb +0 -25
  67. data/lib/usps/imis/party_data.rb +0 -93
  68. data/lib/usps/imis/properties.rb +0 -60
  69. data/lib/usps/imis/query.rb +0 -153
  70. data/lib/usps/imis/requests.rb +0 -68
  71. data/spec/support/usps/vcr/config.rb +0 -47
  72. data/spec/support/usps/vcr/filters.rb +0 -89
  73. data/spec/support/usps/vcr.rb +0 -8
data/lib/usps/imis/api.rb CHANGED
@@ -1,280 +1,207 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'requests'
4
- require_relative 'business_object'
5
- require_relative 'mapper'
6
- require_relative 'query'
7
-
8
3
  module Usps
9
4
  module Imis
10
- # The core API wrapper
11
- #
12
5
  class Api
13
- include Requests
14
-
15
- # Endpoint for (re-)authentication requests
16
- #
17
6
  AUTHENTICATION_PATH = 'Token'
7
+ API_PATH = 'api'
8
+ QUERY_PATH = 'api/Query'
9
+ PANELS = Struct.new(:vsc, :education)
18
10
 
19
- # API bearer token
20
- #
21
- attr_reader :token
22
-
23
- # Expiration time for the API bearer token
24
- #
25
- # Used to automatically re-authenticate as needed
26
- #
27
- attr_reader :token_expiration
28
-
29
- # Currently selected iMIS ID for API requests
30
- #
31
- attr_reader :imis_id
32
-
33
- # Whether to lock changes to the selected iMIS ID
34
- #
35
- attr_reader :lock_imis_id
36
-
37
- # Tagged logger
38
- #
39
- attr_reader :logger
11
+ attr_reader :token, :token_expiration, :imis_id
40
12
 
41
- # Create a new instance of +Api+ and provide an existing auth token
42
- #
43
- # @param token [String] Auth token
44
- #
45
- def self.with_token(token)
46
- new.tap do |api|
47
- api.instance_variable_set(:@token, token)
48
- api.instance_variable_set(:@token_expiration, Time.now + 3600) # Greater than the actual lifetime of the token
49
- end
50
- end
51
-
52
- # A new instance of +Api+
53
- #
54
- # @param imis_id [Integer, String] iMIS ID to select immediately on initialization
55
- #
56
- def initialize(imis_id: nil, record_id: nil)
13
+ def initialize(skip_authentication: false, imis_id: nil)
14
+ authenticate unless skip_authentication
57
15
  self.imis_id = imis_id if imis_id
58
- self.record_id = record_id if record_id
59
- @logger ||= Imis.logger('Api')
60
- Imis.config.validate!
61
16
  end
62
17
 
63
18
  # Manually set the current ID, if you already have it for a given member
64
19
  #
65
- # Supports integer ID and UUID string
66
- #
67
- # @param id [Integer, String] iMIS ID to select for future requests
68
- #
69
- # @return [Integer] iMIS ID
70
- #
71
20
  def imis_id=(id)
72
- raise Errors::LockedIdError if lock_imis_id
73
-
74
- hex = '[0-9a-fA-F]'
75
- uuid_pattern = /^#{hex}{8}-#{hex}{4}-#{hex}{4}-#{hex}{4}-#{hex}{12}$/
76
- @imis_id =
77
- if id.to_s.match?(uuid_pattern)
78
- id
79
- elsif id.to_i.to_s == id.to_s.gsub(' ', '')
80
- id.to_i
81
- end
21
+ @imis_id = id.to_i.to_s
82
22
  end
83
23
 
84
- # Manually set the current record ID
85
- #
86
- # @param id [Integer, String] Record ID to select for future requests
87
- #
88
- # @return [Integer] Record ID
89
- #
90
- def record_id=(id)
91
- return if id.nil?
92
-
93
- raise Errors::LockedIdError if lock_imis_id
94
-
95
- @record_id = id.to_i
96
- end
97
-
98
- # Currently selected Record ID for API requests
99
- #
100
- # Defaults to the iMIS ID
101
- #
102
- def record_id = @record_id || imis_id
103
-
104
24
  # Convert a member's certificate number into an iMIS ID number
105
25
  #
106
- # @param certificate [String] Certificate number to lookup the corresponding iMIS ID for
107
- #
108
- # @return [Integer] Corresponding iMIS ID
109
- #
110
26
  def imis_id_for(certificate)
111
- raise Errors::LockedIdError if lock_imis_id
112
-
113
- logger.debug "Fetching iMIS ID for #{certificate}"
114
-
115
- begin
116
- result = query(Imis.configuration.imis_id_query_name, { certificate: })
117
- page = result.page.tap { logger.tagged('Response').debug it }
118
- self.imis_id = page.first['ID'].to_i
119
- rescue StandardError
120
- raise Errors::NotFoundError, 'Member not found'
121
- end
27
+ result = query(Imis.configuration.imis_id_query_name, { certificate: })
28
+ @imis_id = result['Items']['$values'][0]['ID']
29
+ rescue StandardError
30
+ raise Error::Api, 'Member not found'
122
31
  end
123
32
 
124
33
  # Run requests as DSL, with specific iMIS ID only maintained for this scope
125
34
  #
126
- # Either +id+ or +certificate+ is required.
127
- #
128
- # While in this block, changes to the value of +imis_id+ are not allowed
129
- #
130
- # If no block is given, this sets the iMIS ID and returns self.
131
- #
132
- # @param id [Integer, String] iMIS ID to select for requests within the block
133
- # @param certificate [String] Certificate number to convert to iMIS ID and select for requests within the block
134
- # @param record_id [Integer] Record ID to select for requests within the block
35
+ # This should be used with methods that do not change the value of `imis_id`
135
36
  #
136
- # @example
137
- # with(12345) do
138
- # update(mm: 15)
139
- # end
140
- #
141
- # @return [Usps::Imis::Api]
142
- #
143
- def with(id = nil, certificate: nil, record_id: nil, &)
144
- raise ArgumentError, 'Must provide id or certificate' unless id || certificate
145
-
37
+ def with(id, &)
146
38
  old_id = imis_id
147
- old_record_id = @record_id
148
-
149
- id.nil? ? imis_id_for(certificate) : self.imis_id = id
150
- self.record_id = record_id
151
- return self unless block_given?
152
-
153
- @lock_imis_id = true
39
+ self.imis_id = id
154
40
  instance_eval(&)
155
41
  ensure
156
- if block_given?
157
- @lock_imis_id = false
158
- self.imis_id = old_id
159
- self.record_id = old_record_id
160
- end
42
+ self.imis_id = old_id
161
43
  end
162
44
 
163
- # Build a Query interface
45
+ # Get a business object for the current member
164
46
  #
165
- # Works with IQA queries and Business Objects
166
- #
167
- # @param query_name [String] Full path of the query, e.g. +$/_ABC/Fiander/iMIS_ID+
168
- # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
169
- #
170
- # @return [Usps::Imis::Query] Query wrapper
171
- #
172
- def query(query_name, query_params = nil) = Query.new(self, query_name, **query_params)
47
+ def get(business_object_name, url_id: nil)
48
+ uri = uri_for(business_object_name, url_id:)
49
+ request = Net::HTTP::Get.new(uri)
50
+ result = submit(uri, authorize(request))
51
+ JSON.parse(result.body)
52
+ end
173
53
 
174
- # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
175
- #
176
- # If no block is given, this returns the specified +BusinessObject+.
177
- #
178
- # @param business_object_name [String] Name of the business object
179
- # @param ordinal [Integer] Ordinal to build override ID param of the URL (e.g. used for Panels)
54
+ # Update only specific fields on a business object for the current member
180
55
  #
181
- # @return [Usps::Imis::BusinessObject]
56
+ # fields - hash of shape: { field_name => new_value }
182
57
  #
183
- def on(business_object_name, ordinal: nil, &)
184
- object = BusinessObject.new(self, business_object_name, ordinal:)
185
- return object unless block_given?
186
-
187
- object.instance_eval(&)
58
+ def put_fields(business_object_name, fields, url_id: nil)
59
+ updated = filter_fields(business_object_name, fields)
60
+ put(business_object_name, updated, url_id:)
188
61
  end
189
62
 
190
- # An instance of +Mapper+, using this instance as its parent +Api+
63
+ # Update a business object for the current member
191
64
  #
192
- def mapper
193
- @mapper ||= Mapper.new(self)
65
+ def put(business_object_name, body, url_id: nil)
66
+ uri = uri_for(business_object_name, url_id:)
67
+ request = Net::HTTP::Put.new(uri)
68
+ request.body = JSON.dump(body)
69
+ result = submit(uri, authorize(request))
70
+ JSON.parse(result.body)
194
71
  end
195
72
 
196
- # Convenience alias for reading mapped fields
197
- #
198
- # @return Value of the specified field
199
- #
200
- def fetch(field_key) = mapper.fetch(field_key)
201
- alias [] fetch
202
-
203
- # Convenience alias for reading multiple mapped fields
73
+ # Create a business object for the current member
204
74
  #
205
- # @return [Array] Values of the specified fields
206
- #
207
- def fetch_all(*fields) = mapper.fetch_all(*fields)
208
-
209
- # Convenience alias for updating mapped fields
210
- #
211
- # @return [Array] Array containing the updated object
212
- #
213
- def put_field(field_key, value) = update(field_key => value)
214
- alias []= put_field
75
+ def post(business_object_name, body, url_id: nil)
76
+ uri = uri_for(business_object_name, url_id:)
77
+ request = Net::HTTP::Post.new(uri)
78
+ request.body = JSON.dump(body)
79
+ result = submit(uri, authorize(request))
80
+ JSON.parse(result.body)
81
+ end
215
82
 
216
- # Convenience alias for updating mapped fields
83
+ # Remove a business object for the current member
217
84
  #
218
- # @return [Usps::Imis::Data] Updated object
85
+ # Returns empty string on success
219
86
  #
220
- def update(data) = mapper.update(data)
87
+ def delete(business_object_name, url_id: nil)
88
+ uri = uri_for(business_object_name, url_id:)
89
+ request = Net::HTTP::Delete.new(uri)
90
+ result = submit(uri, authorize(request))
91
+ result.body
92
+ end
221
93
 
222
- # List of available Business Object names
94
+ # Run an IQA Query
223
95
  #
224
- # @return [Hash<Symbol, Array<String>>] Grouped available Business Objects
96
+ # query_name - the full path of the query in IQA, e.g. `$/_ABC/Fiander/iMIS_ID`
97
+ # query_params - hash of shape: { param_name => param_value }
225
98
  #
226
- def business_objects
227
- abc, other = query('BOEntityDefinition').map(&:entity).partition { it.include?('ABC') }
99
+ def query(query_name, query_params = {})
100
+ query_params[:QueryName] = query_name
101
+ path = "#{QUERY_PATH}?#{query_params.to_query}"
102
+ uri = URI(File.join(imis_hostname, path))
103
+ request = Net::HTTP::Get.new(uri)
104
+ result = submit(uri, authorize(request))
105
+ JSON.parse(result.body)
106
+ end
228
107
 
229
- { abc:, other: }
108
+ def mapper
109
+ @mapper ||= Mapper.new(self)
230
110
  end
231
111
 
232
- # Convenience accessor for available Panel objects, each using this instance as its parent
233
- # +Api+
234
- #
235
112
  def panels
236
- @panels ||= Panels.all(self)
113
+ @panels ||= PANELS.new(
114
+ Panel::Vsc.new(self),
115
+ Panel::Education.new(self)
116
+ )
237
117
  end
238
118
 
239
- # Used by the CLI wrappers to reduce authentication requests
240
- #
241
- def auth_token
242
- authenticate
243
-
244
- { token: @token, token_expiration: @token_expiration }
119
+ def update(data)
120
+ mapper.update(data)
245
121
  end
246
122
 
247
- # Ruby 3.5 instance variable filter
248
- #
249
123
  def instance_variables_to_inspect = %i[@token_expiration @imis_id]
250
124
 
251
125
  private
252
126
 
253
- # Authenticate to the iMIS API, and store the access token and expiration time
127
+ def client(uri)
128
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
129
+ http.use_ssl = true
130
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
131
+ end
132
+ end
133
+
134
+ def imis_hostname
135
+ Imis.configuration.hostname
136
+ end
137
+
138
+ # Authorize a request prior to submitting
254
139
  #
255
- def authenticate
256
- json = nil # Scope
140
+ # If the current token is missing/expired, request a new one
141
+ #
142
+ def authorize(request)
143
+ authenticate if token_expiration < Time.now
144
+ request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
145
+ end
257
146
 
258
- logger.tagged('Auth') do
259
- logger.info 'Authenticating with iMIS'
147
+ # Construct a business object API endpoint address
148
+ #
149
+ def uri_for(business_object_name, url_id: nil)
150
+ url_id ||= imis_id
151
+ url_id = CGI.escape(url_id)
152
+ URI(File.join(imis_hostname, "#{API_PATH}/#{business_object_name}/#{url_id}"))
153
+ end
260
154
 
261
- request = http_post
262
- request.body = URI.encode_www_form(
263
- grant_type: 'password',
264
- username: Imis.configuration.username,
265
- password: Imis.configuration.password
266
- )
267
- result = submit(uri, request)
268
- json = JSON.parse(result.body)
155
+ def submit(uri, request)
156
+ client(uri).request(request).tap do |result|
157
+ raise Error::Response.from(result) unless result.is_a?(Net::HTTPSuccess)
269
158
  end
159
+ end
160
+
161
+ # Authenticate to the iMIS API, and store the access token and expiration time
162
+ #
163
+ def authenticate
164
+ uri = URI(File.join(imis_hostname, AUTHENTICATION_PATH))
165
+ req = Net::HTTP::Post.new(uri)
166
+ authentication_data = {
167
+ grant_type: 'password',
168
+ username: Imis.configuration.username,
169
+ password: Imis.configuration.password
170
+ }
171
+ req.body = URI.encode_www_form(authentication_data)
172
+ result = submit(uri, req)
173
+ json = JSON.parse(result.body)
270
174
 
271
175
  @token = json['access_token']
272
- @token_expiration = Time.now + json['expires_in'] - 60
176
+ @token_expiration = Time.parse(json['.expires'])
273
177
  end
274
178
 
275
- # URI for the authentication endpoint
179
+ # Manually assemble the matching data structure, with fields in the correct order
276
180
  #
277
- def uri(...) = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
181
+ def filter_fields(business_object_name, fields)
182
+ existing = get(business_object_name)
183
+
184
+ JSON.parse(JSON.dump(existing)).tap do |updated|
185
+ # The first property is always the iMIS ID again
186
+ updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
187
+
188
+ # Iterate through all existing fields
189
+ existing['Properties']['$values'].each do |value|
190
+ next unless fields.keys.include?(value['Name'])
191
+
192
+ # Strings are not wrapped in the type definition structure
193
+ new_value = fields[value['Name']]
194
+ if new_value.is_a?(String)
195
+ value['Value'] = new_value
196
+ else
197
+ value['Value']['$value'] = new_value
198
+ end
199
+
200
+ # Add the completed field with the updated value
201
+ updated['Properties']['$values'] << value
202
+ end
203
+ end
204
+ end
278
205
  end
279
206
  end
280
207
  end
@@ -1,87 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'logger'
4
- require_relative 'logger_formatter'
5
- require_relative 'logger_helpers'
6
-
7
3
  module Usps
8
4
  module Imis
9
- # API Configuration
10
- #
11
5
  class Config
12
6
  IMIS_ROOT_URL_PROD = 'https://portal.americasboatingclub.org'
13
7
  IMIS_ROOT_URL_DEV = 'https://abcdev.imiscloud.com'
14
- REQUIRED_CONFIGS = %w[imis_id_query_name username password].freeze
15
8
 
16
- attr_accessor :imis_id_query_name, :username, :password
17
- attr_reader :environment, :logger, :logger_level, :logger_file
9
+ attr_accessor :environment, :imis_id_query_name, :username, :password
18
10
 
19
11
  def initialize
20
- @environment = default_environment
21
- @imis_id_query_name = ENV.fetch('IMIS_ID_QUERY_NAME', nil)
22
- @username = ENV.fetch('IMIS_USERNAME', nil)
23
- @password = ENV.fetch('IMIS_PASSWORD', nil)
24
- @base_logger = Logger.new($stdout, level: :info)
25
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
26
-
27
12
  yield self if block_given?
28
-
29
- @logger_level = logger.class::SEV_LABEL[logger.level].downcase.to_sym
30
- end
31
-
32
- def environment=(env)
33
- @environment = ActiveSupport::StringInquirer.new(env.to_s)
34
- end
35
-
36
- def logger=(logger)
37
- @base_logger = logger.tap { it.formatter = LoggerFormatter.new }
38
- @base_logger.singleton_class.include(LoggerHelpers)
39
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
40
- end
41
-
42
- def logger_file=(path)
43
- @logger_file = path
44
- @base_logger = Logger.new(@logger_file.nil? ? $stdout : @logger_file, level: logger.level)
45
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
46
- end
47
-
48
- def silence!
49
- self.logger = Logger.new(nil)
50
13
  end
51
14
 
52
- # Environment-specific API endpoint hostname
53
- #
54
- # @return The API hostname for the current environment
55
- #
56
15
  def hostname
57
- return IMIS_ROOT_URL_PROD if environment.production?
58
- return IMIS_ROOT_URL_DEV if environment.development?
59
-
60
- raise Errors::ConfigError, "Unexpected API environment: #{environment}"
61
- end
62
-
63
- # Ruby 3.5 instance variable filter
64
- #
65
- def instance_variables_to_inspect = instance_variables - %i[@password @base_logger @logger]
66
-
67
- # Parameters to filter out of logging
68
- #
69
- def filtered_parameters = %i[password]
70
-
71
- def validate!
72
- missing_config = REQUIRED_CONFIGS.filter_map { it if public_send(it).nil? }
73
- return if missing_config.empty?
74
-
75
- raise Errors::ConfigError, "Missing required configuration: #{missing_config.join(', ')}"
16
+ case environment.to_sym
17
+ when :production
18
+ IMIS_ROOT_URL_PROD
19
+ when :development
20
+ IMIS_ROOT_URL_DEV
21
+ else
22
+ raise Error::Api, "Unexpected API environment: #{environment}"
23
+ end
76
24
  end
77
25
 
78
- private
79
-
80
- def default_environment
81
- return ::Rails.env if defined?(::Rails)
82
-
83
- ActiveSupport::StringInquirer.new(ENV.fetch('IMIS_ENVIRONMENT', 'development'))
84
- end
26
+ def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username]
85
27
  end
86
28
  end
87
29
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Error
6
+ class Api < StandardError
7
+ attr_accessor :metadata
8
+
9
+ def initialize(message, metadata = {})
10
+ super(message)
11
+ @metadata = metadata
12
+ end
13
+
14
+ def bugsnag_meta_data
15
+ metadata == {} ? {} : base_metadata
16
+ end
17
+
18
+ private
19
+
20
+ def base_metadata
21
+ { api: metadata }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Error
6
+ class Mapper < Api; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Error
6
+ class Response < Api
7
+ attr_reader :response
8
+ attr_accessor :metadata
9
+
10
+ def self.from(response)
11
+ new(nil, response)
12
+ end
13
+
14
+ def initialize(_message, response, metadata = {})
15
+ @response = response
16
+ super(message, metadata)
17
+ end
18
+
19
+ def bugsnag_meta_data
20
+ base_metadata.tap { |m| m[:api].merge!(metadata) }
21
+ end
22
+
23
+ def message
24
+ [
25
+ "#{self.class.name}: [#{status.to_s.upcase}] The iMIS API returned an error.",
26
+ (metadata.inspect if metadata != {}),
27
+ body
28
+ ].compact.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def base_metadata
34
+ { api: { status:, body: } }
35
+ end
36
+
37
+ def status
38
+ @status ||=
39
+ case response.code
40
+ when '400'
41
+ :bad_request
42
+ when '401'
43
+ :unauthorized # RequestVerificationToken invalid
44
+ when '404'
45
+ :not_found
46
+ when '422'
47
+ :unprocessable_entity # validation error
48
+ when /^50\d$/
49
+ :internal_server_error # error within iMIS
50
+ else
51
+ response.code
52
+ end
53
+ end
54
+
55
+ def response_body
56
+ @response_body ||= JSON.parse(response.body)
57
+ rescue StandardError
58
+ @response_body ||= response.body
59
+ end
60
+
61
+ def body
62
+ return response_body unless response_body.is_a?(Hash)
63
+
64
+ case response_body['error']
65
+ when 'invalid_grant'
66
+ response_body['error_description']
67
+ else
68
+ # Unknown error type: just use the raw response
69
+ response.body
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end