usps-imis-api 1.0.0.pre.rc.2 → 1.0.0.pre.rc.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd103cb82cf705d717bd293bef46390aee8a82dca66ec103fa28510bb67b921c
4
- data.tar.gz: 4d1dee0de5129874d612b8b888d57b24918582925b61ffac91ef7722a5cbc9cd
3
+ metadata.gz: bb187f7e4a8497da991378eec6a9893c5c2a5e637b808a9225ab72ebd5ee385d
4
+ data.tar.gz: 0d09118f25fce28b94ddbc1257e9670fe3454e34df094885bc9cf35cbb1a86a0
5
5
  SHA512:
6
- metadata.gz: 30acc54f0082681a9c38ba36bdfe6252331a0199416e131557352a880df66b465cd30de07fb3c3ab0d66366df97b096210e58be649387a3929609c09b9293010
7
- data.tar.gz: 729b191b9d78e62f0acc659730d3761e4dc8a60f72304dd5b584a6ac9a63853c659c0fc335ceb7cbd35d903bc2c85656c0642259b2ee8c2f318669741d0d31fe
6
+ metadata.gz: f50698bc529a6d66d3c69e201d696cdfde1f24c7bf414ab1f655ebdf3b21e1bd512b8e00476ffa62e3ded1a50d72e82586b8f11c002291c5a8038ad5f4cdd2d6
7
+ data.tar.gz: '09e2849e666c312d0a974efc14b5133ac6e2f0ef181a6715f83ae420b6ba48f6a1566cc749a1dcbce614989c9e5b9fa343ba4dd6a56f1ecd47a9a14b7572617a'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- usps-imis-api (1.0.0.pre.rc.2)
4
+ usps-imis-api (1.0.0.pre.rc.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/Readme.md CHANGED
@@ -13,7 +13,7 @@ gem install usps-imis-api
13
13
  or add this line to your Gemfile:
14
14
 
15
15
  ```ruby
16
- gem 'usps-imis-api', '>= 0.4.0'
16
+ gem 'usps-imis-api', '>= 0.6.0'
17
17
  ```
18
18
 
19
19
  ## Setup
@@ -82,7 +82,7 @@ To fetch member data, run e.g.:
82
82
  ```ruby
83
83
  api.imis_id = 31092
84
84
 
85
- data = api.get('ABC_ASC_Individual_Demog')
85
+ data = api.on('ABC_ASC_Individual_Demog').get
86
86
  ```
87
87
 
88
88
  ### PUT Fields
@@ -93,7 +93,7 @@ To update member data, run e.g.:
93
93
  api.imis_id = 31092
94
94
 
95
95
  data = { 'MMS_Updated' => Time.now.strftime('%Y-%m-%dT%H:%M:%S'), 'TotMMS' => new_total }
96
- update = api.put_fields('ABC_ASC_Individual_Demog', data)
96
+ update = api.on('ABC_ASC_Individual_Demog').put_fields(data)
97
97
  ```
98
98
 
99
99
  This method fetches the current data structure, and filters it down to just what you want to
@@ -106,7 +106,7 @@ To update member data, run e.g.:
106
106
  ```ruby
107
107
  api.imis_id = 31092
108
108
 
109
- update = api.put('ABC_ASC_Individual_Demog', complete_imis_object)
109
+ update = api.on('ABC_ASC_Individual_Demog').put(complete_imis_object)
110
110
  ```
111
111
 
112
112
  This method requires a complete iMIS data structure.
@@ -116,7 +116,7 @@ This method requires a complete iMIS data structure.
116
116
  To create new member data, run e.g.:
117
117
 
118
118
  ```ruby
119
- created = api.post('ABC_ASC_Individual_Demog', complete_imis_object)
119
+ created = api.on('ABC_ASC_Individual_Demog').post(complete_imis_object)
120
120
  ```
121
121
 
122
122
  This method requires a complete iMIS data structure.
@@ -128,7 +128,7 @@ To remove member data, run e.g.:
128
128
  ```ruby
129
129
  api.imis_id = 31092
130
130
 
131
- api.delete('ABC_ASC_Individual_Demog')
131
+ api.on('ABC_ASC_Individual_Demog').delete
132
132
  ```
133
133
 
134
134
  This returns a blank string on success.
@@ -202,9 +202,11 @@ previous value.
202
202
 
203
203
  ```ruby
204
204
  api.with(31092) do
205
- # These three requests are identical:
205
+ # These four requests are identical:
206
206
 
207
- put('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 })
207
+ on('ABC_ASC_Individual_Demog') { put('TotMMS' => 15) }
208
+
209
+ on('ABC_ASC_Individual_Demog').put('TotMMS' => 15)
208
210
 
209
211
  mapper.update(mm: 15)
210
212
 
data/lib/usps/imis/api.rb CHANGED
@@ -5,14 +5,12 @@ module Usps
5
5
  # The core API wrapper
6
6
  #
7
7
  class Api
8
+ include Requests
9
+
8
10
  # Endpoint for (re-)authentication requests
9
11
  #
10
12
  AUTHENTICATION_PATH = 'Token'
11
13
 
12
- # Endpoint for general API requests
13
- #
14
- API_PATH = 'api'
15
-
16
14
  # Endpoint for IQA query requests
17
15
  #
18
16
  QUERY_PATH = 'api/Query'
@@ -31,6 +29,10 @@ module Usps
31
29
  #
32
30
  attr_reader :imis_id
33
31
 
32
+ # Whether to lock changes to the selected iMIS ID
33
+ #
34
+ attr_reader :lock_imis_id
35
+
34
36
  # A new instance of +Api+
35
37
  #
36
38
  # @param skip_authentication [bool] Skip authentication on initialization (used for tests)
@@ -46,7 +48,9 @@ module Usps
46
48
  # @param id [Integer, String] iMIS ID to select for future requests
47
49
  #
48
50
  def imis_id=(id)
49
- @imis_id = id.to_i.to_s
51
+ raise Error::ApiError, 'Cannot change iMIS ID while locked' if lock_imis_id
52
+
53
+ @imis_id = id&.to_i&.to_s
50
54
  end
51
55
 
52
56
  # Convert a member's certificate number into an iMIS ID number
@@ -56,15 +60,21 @@ module Usps
56
60
  # @return [String] Corresponding iMIS ID
57
61
  #
58
62
  def imis_id_for(certificate)
59
- result = query(Imis.configuration.imis_id_query_name, { certificate: })
60
- @imis_id = result['Items']['$values'][0]['ID']
61
- rescue StandardError
62
- raise Error::Api, 'Member not found'
63
+ raise Error::ApiError, 'Cannot change iMIS ID while locked' if lock_imis_id
64
+
65
+ begin
66
+ result = query(Imis.configuration.imis_id_query_name, { certificate: })
67
+ @imis_id = result['Items']['$values'][0]['ID']
68
+ rescue StandardError
69
+ raise Error::ApiError, 'Member not found'
70
+ end
63
71
  end
64
72
 
65
73
  # Run requests as DSL, with specific iMIS ID only maintained for this scope
66
74
  #
67
- # This should be used with methods that do not change the value of `imis_id`
75
+ # While in this block, changes to the value of +imis_id+ are not allowed
76
+ #
77
+ # If no block is given, this sets the iMIS ID and returns self.
68
78
  #
69
79
  # @param id [Integer, String] iMIS ID to select for requests within the block
70
80
  #
@@ -76,104 +86,68 @@ module Usps
76
86
  def with(id, &)
77
87
  old_id = imis_id
78
88
  self.imis_id = id
89
+ return self unless block_given?
90
+
91
+ @lock_imis_id = true
79
92
  instance_eval(&)
80
93
  ensure
81
- self.imis_id = old_id
94
+ if block_given?
95
+ @lock_imis_id = false
96
+ self.imis_id = old_id
97
+ end
82
98
  end
83
99
 
84
- # Get a business object for the current member
100
+ # Run an IQA Query
85
101
  #
86
- # @param business_object_name [String] Name of the business object
87
- # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
102
+ # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
103
+ # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
88
104
  #
89
105
  # @return [Hash] Response data from the API
90
106
  #
91
- def get(business_object_name, url_id: nil)
92
- uri = uri_for(business_object_name, url_id:)
107
+ def query(query_name, query_params = {})
108
+ query_params[:QueryName] = query_name
109
+ path = "#{QUERY_PATH}?#{query_params.to_query}"
110
+ uri = URI(File.join(Imis.configuration.hostname, path))
93
111
  request = Net::HTTP::Get.new(uri)
94
112
  result = submit(uri, authorize(request))
95
113
  JSON.parse(result.body)
96
114
  end
97
115
 
98
- # Update only specific fields on a business object for the current member
116
+ # An instance of +BusinessObject+, using this instance as its parent +Api+
99
117
  #
100
118
  # @param business_object_name [String] Name of the business object
101
- # @param fields [Hash] Conforms to pattern +{ field_key => value }+
102
119
  # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
103
120
  #
104
- # @return [Hash] Response data from the API
105
- #
106
- def put_fields(business_object_name, fields, url_id: nil)
107
- updated = filter_fields(business_object_name, fields)
108
- put(business_object_name, updated, url_id:)
121
+ def business_object(business_object_name, url_id: nil)
122
+ BusinessObject.new(self, business_object_name, url_id:)
109
123
  end
110
124
 
111
- # Update a business object for the current member
125
+ # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
112
126
  #
113
- # @param business_object_name [String] Name of the business object
114
- # @param body [Hash] Full raw API object data
115
- # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
116
- #
117
- # @return [Hash] Response data from the API
118
- #
119
- def put(business_object_name, body, url_id: nil)
120
- uri = uri_for(business_object_name, url_id:)
121
- request = Net::HTTP::Put.new(uri)
122
- request.body = JSON.dump(body)
123
- result = submit(uri, authorize(request))
124
- JSON.parse(result.body)
125
- end
126
-
127
- # Create a business object for the current member
127
+ # If no block is given, this returns the specified +BusinessObject+.
128
128
  #
129
129
  # @param business_object_name [String] Name of the business object
130
- # @param body [Hash] Full raw API object data
131
130
  # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
132
131
  #
133
- # @return [Hash] Response data from the API
134
- #
135
- def post(business_object_name, body, url_id: nil)
136
- uri = uri_for(business_object_name, url_id:)
137
- request = Net::HTTP::Post.new(uri)
138
- request.body = JSON.dump(body)
139
- result = submit(uri, authorize(request))
140
- JSON.parse(result.body)
141
- end
132
+ def on(business_object_name, url_id: nil, &)
133
+ object = business_object(business_object_name, url_id:)
134
+ return object unless block_given?
142
135
 
143
- # Remove a business object for the current member
144
- #
145
- # @param business_object_name [String] Name of the business object
146
- # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
147
- #
148
- # @return [String] Error response body from the API, or empty string on success
149
- #
150
- def delete(business_object_name, url_id: nil)
151
- uri = uri_for(business_object_name, url_id:)
152
- request = Net::HTTP::Delete.new(uri)
153
- result = submit(uri, authorize(request))
154
- result.body
136
+ result = nil
137
+ object.tap { |obj| result = obj.instance_eval(&) }
138
+ result
155
139
  end
156
140
 
157
- # Run an IQA Query
158
- #
159
- # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
160
- # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
141
+ # An instance of +Mapper+, using this instance as its parent +Api+
161
142
  #
162
- # @return [Hash] Response data from the API
163
- #
164
- def query(query_name, query_params = {})
165
- query_params[:QueryName] = query_name
166
- path = "#{QUERY_PATH}?#{query_params.to_query}"
167
- uri = URI(File.join(imis_hostname, path))
168
- request = Net::HTTP::Get.new(uri)
169
- result = submit(uri, authorize(request))
170
- JSON.parse(result.body)
143
+ def mapper
144
+ @mapper ||= Mapper.new(self)
171
145
  end
172
146
 
173
- # An instance of Mapper, using this instance as its parent +Api+
147
+ # Convenience alias for updating mapped fields
174
148
  #
175
- def mapper
176
- @mapper ||= Mapper.new(self)
149
+ def update(data)
150
+ mapper.update(data)
177
151
  end
178
152
 
179
153
  # Convenience accessor for available Panel objects, each using this instance as its parent
@@ -186,56 +160,16 @@ module Usps
186
160
  )
187
161
  end
188
162
 
189
- # Convenience alias for updating mapped fields
190
- #
191
- def update(data)
192
- mapper.update(data)
193
- end
194
-
195
163
  # Ruby 3.5 instance variable filter
196
164
  #
197
165
  def instance_variables_to_inspect = %i[@token_expiration @imis_id]
198
166
 
199
167
  private
200
168
 
201
- def client(uri)
202
- Net::HTTP.new(uri.host, uri.port).tap do |http|
203
- http.use_ssl = true
204
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
205
- end
206
- end
207
-
208
- def imis_hostname
209
- Imis.configuration.hostname
210
- end
211
-
212
- # Authorize a request prior to submitting
213
- #
214
- # If the current token is missing/expired, request a new one
215
- #
216
- def authorize(request)
217
- authenticate if token_expiration < Time.now
218
- request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
219
- end
220
-
221
- # Construct a business object API endpoint address
222
- #
223
- def uri_for(business_object_name, url_id: nil)
224
- url_id ||= imis_id
225
- url_id = CGI.escape(url_id)
226
- URI(File.join(imis_hostname, "#{API_PATH}/#{business_object_name}/#{url_id}"))
227
- end
228
-
229
- def submit(uri, request)
230
- client(uri).request(request).tap do |result|
231
- raise Error::Response.from(result) unless result.is_a?(Net::HTTPSuccess)
232
- end
233
- end
234
-
235
169
  # Authenticate to the iMIS API, and store the access token and expiration time
236
170
  #
237
171
  def authenticate
238
- uri = URI(File.join(imis_hostname, AUTHENTICATION_PATH))
172
+ uri = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
239
173
  req = Net::HTTP::Post.new(uri)
240
174
  authentication_data = {
241
175
  grant_type: 'password',
@@ -249,33 +183,6 @@ module Usps
249
183
  @token = json['access_token']
250
184
  @token_expiration = Time.parse(json['.expires'])
251
185
  end
252
-
253
- # Manually assemble the matching data structure, with fields in the correct order
254
- #
255
- def filter_fields(business_object_name, fields)
256
- existing = get(business_object_name)
257
-
258
- JSON.parse(JSON.dump(existing)).tap do |updated|
259
- # The first property is always the iMIS ID again
260
- updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
261
-
262
- # Iterate through all existing fields
263
- existing['Properties']['$values'].each do |value|
264
- next unless fields.keys.include?(value['Name'])
265
-
266
- # Strings are not wrapped in the type definition structure
267
- new_value = fields[value['Name']]
268
- if new_value.is_a?(String)
269
- value['Value'] = new_value
270
- else
271
- value['Value']['$value'] = new_value
272
- end
273
-
274
- # Add the completed field with the updated value
275
- updated['Properties']['$values'] << value
276
- end
277
- end
278
- end
279
186
  end
280
187
  end
281
188
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ # DEV
6
+ class BusinessObject
7
+ include Requests
8
+
9
+ # Endpoint for general API requests
10
+ #
11
+ API_PATH = 'api'
12
+
13
+ # The parent +Api+ object
14
+ #
15
+ attr_reader :api
16
+
17
+ # Name of the iMIS Business Object
18
+ #
19
+ attr_reader :business_object_name
20
+
21
+ # Override ID param of the URL (e.g. used for Panels)
22
+ #
23
+ attr_reader :url_id
24
+
25
+ # A new instance of +BusinessObject+
26
+ #
27
+ def initialize(api, business_object_name, url_id: nil)
28
+ @api = api
29
+ @business_object_name = business_object_name
30
+ @url_id = url_id
31
+ end
32
+
33
+ # Get a business object for the current member
34
+ #
35
+ # @return [Hash] Response data from the API
36
+ #
37
+ def get
38
+ request = Net::HTTP::Get.new(uri)
39
+ result = submit(uri, authorize(request))
40
+ JSON.parse(result.body)
41
+ end
42
+
43
+ # Update only specific fields on a business object for the current member
44
+ #
45
+ # @param fields [Hash] Conforms to pattern +{ field_key => value }+
46
+ #
47
+ # @return [Hash] Response data from the API
48
+ #
49
+ def put_fields(fields)
50
+ updated = filter_fields(fields)
51
+ put(updated)
52
+ end
53
+
54
+ # Update a business object for the current member
55
+ #
56
+ # @param body [Hash] Full raw API object data
57
+ #
58
+ # @return [Hash] Response data from the API
59
+ #
60
+ def put(body)
61
+ request = Net::HTTP::Put.new(uri)
62
+ request.body = JSON.dump(body)
63
+ result = submit(uri, authorize(request))
64
+ JSON.parse(result.body)
65
+ end
66
+
67
+ # Create a business object for the current member
68
+ #
69
+ # @param body [Hash] Full raw API object data
70
+ #
71
+ # @return [Hash] Response data from the API
72
+ #
73
+ def post(body)
74
+ request = Net::HTTP::Post.new(uri)
75
+ request.body = JSON.dump(body)
76
+ result = submit(uri, authorize(request))
77
+ JSON.parse(result.body)
78
+ end
79
+
80
+ # Remove a business object for the current member
81
+ #
82
+ # @return [String] Error response body from the API, or empty string on success
83
+ #
84
+ def delete
85
+ request = Net::HTTP::Delete.new(uri)
86
+ result = submit(uri, authorize(request))
87
+ result.body
88
+ end
89
+
90
+ private
91
+
92
+ def token = api.token
93
+ def token_expiration = api.token_expiration
94
+
95
+ # Construct a business object API endpoint address
96
+ #
97
+ def uri
98
+ id_for_url = url_id ? CGI.escape(url_id) : api.imis_id
99
+ full_path = "#{API_PATH}/#{business_object_name}/#{id_for_url}"
100
+ URI(File.join(Imis.configuration.hostname, full_path))
101
+ end
102
+
103
+ # Manually assemble the matching data structure, with fields in the correct order
104
+ #
105
+ def filter_fields(fields)
106
+ existing = get
107
+
108
+ JSON.parse(JSON.dump(existing)).tap do |updated|
109
+ # The first property is always the iMIS ID again
110
+ updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
111
+
112
+ # Iterate through all existing fields
113
+ existing['Properties']['$values'].each do |value|
114
+ next unless fields.keys.include?(value['Name'])
115
+
116
+ # Strings are not wrapped in the type definition structure
117
+ new_value = fields[value['Name']]
118
+ if new_value.is_a?(String)
119
+ value['Value'] = new_value
120
+ else
121
+ value['Value']['$value'] = new_value
122
+ end
123
+
124
+ # Add the completed field with the updated value
125
+ updated['Properties']['$values'] << value
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -25,7 +25,7 @@ module Usps
25
25
  when :development
26
26
  IMIS_ROOT_URL_DEV
27
27
  else
28
- raise Error::Api, "Unexpected API environment: #{environment}"
28
+ raise Error::ApiError, "Unexpected API environment: #{environment}"
29
29
  end
30
30
  end
31
31
 
@@ -5,12 +5,12 @@ module Usps
5
5
  module Error
6
6
  # Base error class for all internal exceptions
7
7
  #
8
- class Api < StandardError
8
+ class ApiError < StandardError
9
9
  # Additional call-specific metadata to pass through to Bugsnag
10
10
  #
11
11
  attr_accessor :metadata
12
12
 
13
- # A new instance of +Error::Api+
13
+ # A new instance of +ApiError+
14
14
  #
15
15
  # @param message [String] The base exception message
16
16
  # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
@@ -5,7 +5,7 @@ module Usps
5
5
  module Error
6
6
  # Exception raised by a +Mapper+
7
7
  #
8
- class Mapper < Api; end
8
+ class MapperError < ApiError; end
9
9
  end
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Usps
5
5
  module Error
6
6
  # Exception raised due to receiving an error response from the API
7
7
  #
8
- class Response < Api
8
+ class ResponseError < ApiError
9
9
  # [Net::HTTPResponse] The response received from the API
10
10
  #
11
11
  attr_reader :response
@@ -14,7 +14,7 @@ module Usps
14
14
  #
15
15
  attr_accessor :metadata
16
16
 
17
- # Create a new instance of +Error::Response+ from an API response
17
+ # Create a new instance of +ResponseError+ from an API response
18
18
  #
19
19
  # @param response [Net::HTTPResponse] The response received from the API
20
20
  #
@@ -22,7 +22,7 @@ module Usps
22
22
  new(nil, response)
23
23
  end
24
24
 
25
- # Create a new instance of +Error::Response+
25
+ # Create a new instance of +ResponseError+
26
26
  #
27
27
  # @param _message Ignored
28
28
  # @param response [Net::HTTPResponse] The response received from the API
@@ -47,7 +47,7 @@ module Usps
47
47
  end
48
48
 
49
49
  updates.map do |business_object_name, field_updates|
50
- api.put_fields(business_object_name, field_updates)
50
+ api.business_object(business_object_name).put_fields(field_updates)
51
51
  end
52
52
  end
53
53
 
@@ -74,7 +74,7 @@ module Usps
74
74
  end
75
75
 
76
76
  raise(
77
- Error::Mapper,
77
+ Error::MapperError,
78
78
  "Unrecognized field: \"#{field_name}\". " \
79
79
  'Please report what data you are attempting to work with to ITCom leadership.'
80
80
  )
@@ -20,7 +20,7 @@ module Usps
20
20
  # @param ordinal [Integer] The ordinal identifier for the desired object
21
21
  #
22
22
  def get(ordinal)
23
- api.get(business_object, url_id: "~#{api.imis_id}|#{ordinal}")
23
+ api.business_object(business_object, url_id: "~#{api.imis_id}|#{ordinal}").get
24
24
  end
25
25
 
26
26
  # Create a new object in the Panel
@@ -28,7 +28,7 @@ module Usps
28
28
  # @param data [Hash] The record data for the desired object
29
29
  #
30
30
  def create(data)
31
- api.post(business_object, payload(data), url_id: '')
31
+ api.business_object(business_object, url_id: '').post(payload(data))
32
32
  end
33
33
 
34
34
  # Update an existing object in the Panel
@@ -37,7 +37,9 @@ module Usps
37
37
  # +ordinal+ identifier
38
38
  #
39
39
  def update(data)
40
- api.put(business_object, payload(data), url_id: "~#{api.imis_id}|#{data[:ordinal]}")
40
+ api
41
+ .business_object(business_object, url_id: "~#{api.imis_id}|#{data[:ordinal]}")
42
+ .put(payload(data))
41
43
  end
42
44
 
43
45
  # Remove a specific object from the Panel
@@ -45,17 +47,17 @@ module Usps
45
47
  # @param ordinal [Integer] The ordinal identifier for the desired object
46
48
  #
47
49
  def destroy(ordinal)
48
- api.delete(business_object, url_id: "~#{api.imis_id}|#{ordinal}")
50
+ api.business_object(business_object, url_id: "~#{api.imis_id}|#{ordinal}").delete
49
51
  end
50
52
 
51
53
  private
52
54
 
53
55
  def business_object
54
- raise Error::Api, "#{self.class.name} must implement #business_object"
56
+ raise Error::ApiError, "#{self.class.name} must implement #business_object"
55
57
  end
56
58
 
57
59
  def payload(_data)
58
- raise Error::Api, "#{self.class.name} must implement #payload(data)"
60
+ raise Error::ApiError, "#{self.class.name} must implement #payload(data)"
59
61
  end
60
62
  end
61
63
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Requests
6
+ private
7
+
8
+ def client(uri)
9
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
10
+ http.use_ssl = true
11
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
12
+ end
13
+ end
14
+
15
+ # Authorize a request prior to submitting
16
+ #
17
+ # If the current token is missing/expired, request a new one
18
+ #
19
+ def authorize(request)
20
+ authenticate if token_expiration < Time.now
21
+ request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
22
+ end
23
+
24
+ def submit(uri, request)
25
+ client(uri).request(request).tap do |result|
26
+ raise Error::ResponseError.from(result) unless result.is_a?(Net::HTTPSuccess)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- VERSION = '1.0.0-rc.2'
5
+ VERSION = '1.0.0-rc.4'
6
6
  end
7
7
  end
data/lib/usps/imis.rb CHANGED
@@ -13,9 +13,11 @@ require 'ext/hash' unless defined?(Rails)
13
13
 
14
14
  # Internal requires
15
15
  require_relative 'imis/config'
16
- require_relative 'imis/error/api'
17
- require_relative 'imis/error/mapper'
18
- require_relative 'imis/error/response'
16
+ require_relative 'imis/error/api_error'
17
+ require_relative 'imis/error/mapper_error'
18
+ require_relative 'imis/error/response_error'
19
+ require_relative 'imis/requests'
20
+ require_relative 'imis/business_object'
19
21
  require_relative 'imis/api'
20
22
  require_relative 'imis/mapper'
21
23
  require_relative 'imis/panel/base_panel'
@@ -31,7 +31,7 @@ describe Usps::Imis::Api do
31
31
 
32
32
  it 'wraps errors' do
33
33
  expect { api.imis_id_for('E231625') }.to raise_error(
34
- Usps::Imis::Error::Api, 'Member not found'
34
+ Usps::Imis::Error::ApiError, 'Member not found'
35
35
  )
36
36
  end
37
37
  end
@@ -41,13 +41,15 @@ describe Usps::Imis::Api do
41
41
  before { api.imis_id = 31092 }
42
42
 
43
43
  it 'sends an update' do
44
- expect(api.put_fields('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 })).to be_a(Hash)
44
+ expect(api.business_object('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15)).to(
45
+ be_a(Hash)
46
+ )
45
47
  end
46
48
 
47
49
  context 'when receiving a response error' do
48
50
  let(:warning_text) do
49
51
  <<~WARNING.chomp
50
- Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
52
+ Usps::Imis::Error::ResponseError: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
51
53
  Something went wrong
52
54
  WARNING
53
55
  end
@@ -61,8 +63,8 @@ describe Usps::Imis::Api do
61
63
  end
62
64
 
63
65
  it 'wraps the error' do
64
- expect { api.put_fields('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 }) }.to raise_error(
65
- Usps::Imis::Error::Api, warning_text
66
+ expect { api.business_object('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15) }.to(
67
+ raise_error(Usps::Imis::Error::ApiError, warning_text)
66
68
  )
67
69
  end
68
70
  end
@@ -71,7 +73,7 @@ describe Usps::Imis::Api do
71
73
  describe '#with' do
72
74
  it 'sends an update from put' do
73
75
  expect(
74
- api.with(31092) { put_fields('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 }) }
76
+ api.with(31092) { business_object('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15) }
75
77
  ).to be_a(Hash)
76
78
  end
77
79
 
@@ -82,6 +84,51 @@ describe Usps::Imis::Api do
82
84
  it 'uses a panel correctly' do
83
85
  expect(api.with(6374) { panels.vsc.get(1433) }).to be_a(Hash)
84
86
  end
87
+
88
+ it 'blocks calling imis_id=' do
89
+ expect do
90
+ api.with(31092) { self.imis_id = 31092 }
91
+ end.to raise_error(Usps::Imis::Error::ApiError, 'Cannot change iMIS ID while locked')
92
+ end
93
+
94
+ it 'blocks calling imis_id_for' do
95
+ expect do
96
+ api.with(31092) { imis_id_for('E231625') }
97
+ end.to raise_error(Usps::Imis::Error::ApiError, 'Cannot change iMIS ID while locked')
98
+ end
99
+ end
100
+
101
+ describe '#on' do
102
+ it 'returns a BusinessObject without a block' do
103
+ expect(api.on('ABC_ASC_Individual_Demog')).to be_a(Usps::Imis::BusinessObject)
104
+ end
105
+
106
+ it 'sends an update from put', :aggregate_failures do
107
+ result = api.with(31092) do
108
+ on('ABC_ASC_Individual_Demog') { put_fields({ 'TotMMS' => 15 }) }
109
+ end
110
+
111
+ expect(result).to be_a(Hash)
112
+ expect(api.imis_id).to be_nil
113
+ end
114
+
115
+ it 'chains .with().on() to a single block', :aggregate_failures do
116
+ result = api.with(31092).on('ABC_ASC_Individual_Demog') do
117
+ put_fields({ 'TotMMS' => 15 })
118
+ end
119
+
120
+ expect(result).to be_a(Hash)
121
+ expect(api.imis_id).to eq('31092')
122
+ end
123
+
124
+ it 'nests on and with', :aggregate_failures do
125
+ result = api.on('ABC_ASC_Individual_Demog') do |object|
126
+ api.with(31092) { object.put_fields({ 'TotMMS' => 15 }) }
127
+ end
128
+
129
+ expect(result).to be_a(Hash)
130
+ expect(api.imis_id).to be_nil
131
+ end
85
132
  end
86
133
 
87
134
  describe '#inspect' do
@@ -108,36 +155,4 @@ describe Usps::Imis::Api do
108
155
  expect(api).to have_received(:authenticate)
109
156
  end
110
157
  end
111
-
112
- describe '#filter_fields' do
113
- let(:expected) do
114
- {
115
- 'Properties' => {
116
- '$values' => [
117
- { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
118
- { 'Name' => 'Stub Integer', 'Value' => { '$value' => 43 } },
119
- { 'Name' => 'Stub String', 'Value' => 'other' }
120
- ]
121
- }
122
- }
123
- end
124
-
125
- before do
126
- allow(api).to receive(:get).and_return({
127
- 'Properties' => {
128
- '$values' => [
129
- { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
130
- { 'Name' => 'Stub Integer', 'Value' => { '$value' => 42 } },
131
- { 'Name' => 'Stub String', 'Value' => 'something' }
132
- ]
133
- }
134
- })
135
- end
136
-
137
- it 'formats fields correctly' do
138
- updated = api.send(:filter_fields, 'Stub', { 'Stub Integer' => 43, 'Stub String' => 'other' })
139
-
140
- expect(updated).to eq(expected)
141
- end
142
- end
143
158
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Usps::Imis::BusinessObject do
6
+ let(:business_object) { described_class.new(api, 'Stub') }
7
+ let(:api) { Usps::Imis::Api.new }
8
+
9
+ describe '#filter_fields' do
10
+ let(:expected) do
11
+ {
12
+ 'Properties' => {
13
+ '$values' => [
14
+ { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
15
+ { 'Name' => 'Stub Integer', 'Value' => { '$value' => 43 } },
16
+ { 'Name' => 'Stub String', 'Value' => 'other' }
17
+ ]
18
+ }
19
+ }
20
+ end
21
+
22
+ before do
23
+ allow(business_object).to receive(:get).and_return({
24
+ 'Properties' => {
25
+ '$values' => [
26
+ { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
27
+ { 'Name' => 'Stub Integer', 'Value' => { '$value' => 42 } },
28
+ { 'Name' => 'Stub String', 'Value' => 'something' }
29
+ ]
30
+ }
31
+ })
32
+ end
33
+
34
+ it 'formats fields correctly' do
35
+ updated = business_object.send(:filter_fields, 'Stub Integer' => 43, 'Stub String' => 'other')
36
+
37
+ expect(updated).to eq(expected)
38
+ end
39
+ end
40
+ end
@@ -25,7 +25,7 @@ describe Usps::Imis::Config do
25
25
 
26
26
  it 'raises an error' do
27
27
  expect { config.hostname }.to raise_error(
28
- Usps::Imis::Error::Api, 'Unexpected API environment: nothing'
28
+ Usps::Imis::Error::ApiError, 'Unexpected API environment: nothing'
29
29
  )
30
30
  end
31
31
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Usps::Imis::Error::Api do
5
+ describe Usps::Imis::Error::ApiError do
6
6
  it 'builds Bugsnag metadata' do
7
7
  error = described_class.new('Example', something: :else)
8
8
 
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  ApiResponseStub = Struct.new(:code, :body)
6
6
 
7
- describe Usps::Imis::Error::Response do
7
+ describe Usps::Imis::Error::ResponseError do
8
8
  let(:error) { described_class.from(response) }
9
9
 
10
10
  describe 'error codes' do
@@ -61,7 +61,7 @@ describe Usps::Imis::Error::Response do
61
61
  let(:response) { ApiResponseStub.new('500', 'Body of the API response error') }
62
62
  let(:warning_text) do
63
63
  <<~WARNING.chomp
64
- Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
64
+ Usps::Imis::Error::ResponseError: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
65
65
  Body of the API response error
66
66
  WARNING
67
67
  end
@@ -78,7 +78,7 @@ describe Usps::Imis::Error::Response do
78
78
  let(:response) { ApiResponseStub.new('500', response_body) }
79
79
  let(:warning_text) do
80
80
  <<~WARNING.chomp
81
- Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
81
+ Usps::Imis::Error::ResponseError: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
82
82
  description
83
83
  WARNING
84
84
  end
@@ -95,7 +95,7 @@ describe Usps::Imis::Error::Response do
95
95
  let(:response) { ApiResponseStub.new('500', response_body) }
96
96
  let(:warning_text) do
97
97
  <<~WARNING.chomp
98
- Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
98
+ Usps::Imis::Error::ResponseError: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
99
99
  #{response_body}
100
100
  WARNING
101
101
  end
@@ -22,7 +22,7 @@ describe Usps::Imis::Mapper do
22
22
 
23
23
  it 'raises for unmapped updates' do
24
24
  expect { api.mapper.update(something: 'anything') }.to raise_error(
25
- Usps::Imis::Error::Mapper,
25
+ Usps::Imis::Error::MapperError,
26
26
  'Unrecognized field: "something". ' \
27
27
  'Please report what data you are attempting to work with to ITCom leadership.'
28
28
  )
@@ -19,13 +19,13 @@ end
19
19
  describe Usps::Imis::Panel::BasePanel do
20
20
  it 'requires #business_object to be defined' do
21
21
  expect { Usps::Imis::Panel::InvalidPanel.new.get(1) }.to raise_error(
22
- Usps::Imis::Error::Api, 'Usps::Imis::Panel::InvalidPanel must implement #business_object'
22
+ Usps::Imis::Error::ApiError, 'Usps::Imis::Panel::InvalidPanel must implement #business_object'
23
23
  )
24
24
  end
25
25
 
26
26
  it 'requires #payload(data) to be defined' do
27
27
  expect { Usps::Imis::Panel::InvalidPanelWithBusinessObject.new.create({}) }.to raise_error(
28
- Usps::Imis::Error::Api,
28
+ Usps::Imis::Error::ApiError,
29
29
  'Usps::Imis::Panel::InvalidPanelWithBusinessObject must implement #payload(data)'
30
30
  )
31
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: usps-imis-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.rc.2
4
+ version: 1.0.0.pre.rc.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Fiander
@@ -30,19 +30,22 @@ files:
30
30
  - lib/ext/hash.rb
31
31
  - lib/usps/imis.rb
32
32
  - lib/usps/imis/api.rb
33
+ - lib/usps/imis/business_object.rb
33
34
  - lib/usps/imis/config.rb
34
- - lib/usps/imis/error/api.rb
35
- - lib/usps/imis/error/mapper.rb
36
- - lib/usps/imis/error/response.rb
35
+ - lib/usps/imis/error/api_error.rb
36
+ - lib/usps/imis/error/mapper_error.rb
37
+ - lib/usps/imis/error/response_error.rb
37
38
  - lib/usps/imis/mapper.rb
38
39
  - lib/usps/imis/panel/base_panel.rb
39
40
  - lib/usps/imis/panel/education.rb
40
41
  - lib/usps/imis/panel/vsc.rb
42
+ - lib/usps/imis/requests.rb
41
43
  - lib/usps/imis/version.rb
42
44
  - spec/lib/usps/imis/api_spec.rb
45
+ - spec/lib/usps/imis/business_object_spec.rb
43
46
  - spec/lib/usps/imis/config_spec.rb
44
- - spec/lib/usps/imis/error/api_spec.rb
45
- - spec/lib/usps/imis/error/response_spec.rb
47
+ - spec/lib/usps/imis/error/api_error_spec.rb
48
+ - spec/lib/usps/imis/error/response_error_spec.rb
46
49
  - spec/lib/usps/imis/mapper_spec.rb
47
50
  - spec/lib/usps/imis/panel/base_panel_spec.rb
48
51
  - spec/lib/usps/imis/panel/education_spec.rb