usps-imis-api 0.11.31 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +57 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/.simplecov +8 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +95 -0
- data/Rakefile +12 -0
- data/Readme.md +191 -19
- data/bin/console +21 -0
- data/bin/setup +8 -0
- data/lib/ext/hash.rb +10 -0
- data/lib/usps/imis/api.rb +138 -211
- data/lib/usps/imis/config.rb +10 -68
- data/lib/usps/imis/error/api.rb +26 -0
- data/lib/usps/imis/error/mapper.rb +9 -0
- data/lib/usps/imis/error/response.rb +75 -0
- data/lib/usps/imis/mapper.rb +21 -90
- data/lib/usps/imis/panel/base_panel.rb +42 -0
- data/lib/usps/imis/panel/education.rb +111 -0
- data/lib/usps/imis/panel/vsc.rb +109 -0
- data/lib/usps/imis/version.rb +1 -1
- data/lib/usps/imis.rb +17 -33
- data/spec/lib/usps/imis/api_spec.rb +143 -0
- data/spec/lib/usps/imis/config_spec.rb +33 -0
- data/spec/lib/usps/imis/error/api_spec.rb +17 -0
- data/spec/lib/usps/imis/error/response_spec.rb +107 -0
- data/spec/lib/usps/imis/mapper_spec.rb +31 -0
- data/spec/lib/usps/imis/panel/base_panel_spec.rb +32 -0
- data/spec/lib/usps/imis/panel/education_spec.rb +55 -0
- data/spec/lib/usps/imis/panel/vsc_spec.rb +38 -0
- data/spec/lib/usps/imis_spec.rb +11 -0
- data/spec/spec_helper.rb +35 -0
- data/usps-imis-api.gemspec +18 -0
- metadata +33 -98
- data/bin/imis +0 -8
- data/lib/usps/imis/base_data.rb +0 -68
- data/lib/usps/imis/blank_object.rb +0 -62
- data/lib/usps/imis/business_object.rb +0 -230
- data/lib/usps/imis/command_line/interface.rb +0 -165
- data/lib/usps/imis/command_line/options_parser.rb +0 -139
- data/lib/usps/imis/command_line/performers.rb +0 -80
- data/lib/usps/imis/command_line.rb +0 -15
- data/lib/usps/imis/data.rb +0 -60
- data/lib/usps/imis/error.rb +0 -55
- data/lib/usps/imis/errors/api_error.rb +0 -11
- data/lib/usps/imis/errors/command_line_error.rb +0 -11
- data/lib/usps/imis/errors/config_error.rb +0 -11
- data/lib/usps/imis/errors/locked_id_error.rb +0 -15
- data/lib/usps/imis/errors/mapper_error.rb +0 -29
- data/lib/usps/imis/errors/missing_id_error.rb +0 -15
- data/lib/usps/imis/errors/not_found_error.rb +0 -11
- data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
- data/lib/usps/imis/errors/response_error.rb +0 -104
- data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
- data/lib/usps/imis/logger.rb +0 -19
- data/lib/usps/imis/logger_formatter.rb +0 -32
- data/lib/usps/imis/logger_helpers.rb +0 -20
- data/lib/usps/imis/mocks/business_object.rb +0 -47
- data/lib/usps/imis/mocks.rb +0 -11
- data/lib/usps/imis/panels/base_panel.rb +0 -125
- data/lib/usps/imis/panels/education.rb +0 -29
- data/lib/usps/imis/panels/vsc.rb +0 -28
- data/lib/usps/imis/panels.rb +0 -25
- data/lib/usps/imis/party_data.rb +0 -93
- data/lib/usps/imis/properties.rb +0 -60
- data/lib/usps/imis/query.rb +0 -153
- data/lib/usps/imis/requests.rb +0 -68
- data/spec/support/usps/vcr/config.rb +0 -47
- data/spec/support/usps/vcr/filters.rb +0 -89
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
45
|
+
# Get a business object for the current member
|
|
164
46
|
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
56
|
+
# fields - hash of shape: { field_name => new_value }
|
|
182
57
|
#
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
#
|
|
63
|
+
# Update a business object for the current member
|
|
191
64
|
#
|
|
192
|
-
def
|
|
193
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
#
|
|
83
|
+
# Remove a business object for the current member
|
|
217
84
|
#
|
|
218
|
-
#
|
|
85
|
+
# Returns empty string on success
|
|
219
86
|
#
|
|
220
|
-
def
|
|
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
|
-
#
|
|
94
|
+
# Run an IQA Query
|
|
223
95
|
#
|
|
224
|
-
#
|
|
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
|
|
227
|
-
|
|
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
|
-
|
|
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 ||=
|
|
113
|
+
@panels ||= PANELS.new(
|
|
114
|
+
Panel::Vsc.new(self),
|
|
115
|
+
Panel::Education.new(self)
|
|
116
|
+
)
|
|
237
117
|
end
|
|
238
118
|
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
176
|
+
@token_expiration = Time.parse(json['.expires'])
|
|
273
177
|
end
|
|
274
178
|
|
|
275
|
-
#
|
|
179
|
+
# Manually assemble the matching data structure, with fields in the correct order
|
|
276
180
|
#
|
|
277
|
-
def
|
|
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
|
data/lib/usps/imis/config.rb
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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,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
|