usps-imis-api 1.0.0.pre.rc.8 → 1.0.0.pre.rc.10

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +3 -323
  3. data/lib/usps/imis/api.rb +21 -17
  4. data/lib/usps/imis/business_object.rb +18 -18
  5. data/lib/usps/imis/config.rb +6 -2
  6. data/lib/usps/imis/data.rb +4 -0
  7. data/lib/usps/imis/error.rb +2 -1
  8. data/lib/usps/imis/errors/missing_id_error.rb +15 -0
  9. data/lib/usps/imis/panels/base_panel.rb +2 -1
  10. data/lib/usps/imis/properties.rb +14 -14
  11. data/lib/usps/imis/query.rb +78 -25
  12. data/lib/usps/imis/requests.rb +12 -4
  13. data/lib/usps/imis/version.rb +1 -1
  14. data/spec/support/usps/vcr/config.rb +47 -0
  15. data/spec/support/usps/vcr/filters.rb +89 -0
  16. data/spec/support/usps/vcr.rb +8 -0
  17. metadata +6 -27
  18. data/.github/workflows/main.yml +0 -57
  19. data/.gitignore +0 -5
  20. data/.rspec +0 -2
  21. data/.rubocop.yml +0 -89
  22. data/.ruby-version +0 -1
  23. data/.simplecov +0 -8
  24. data/Gemfile +0 -12
  25. data/Gemfile.lock +0 -129
  26. data/Rakefile +0 -12
  27. data/bin/console +0 -21
  28. data/bin/setup +0 -8
  29. data/spec/lib/usps/imis/api_spec.rb +0 -171
  30. data/spec/lib/usps/imis/business_object_spec.rb +0 -87
  31. data/spec/lib/usps/imis/config_spec.rb +0 -59
  32. data/spec/lib/usps/imis/data_spec.rb +0 -66
  33. data/spec/lib/usps/imis/error_spec.rb +0 -17
  34. data/spec/lib/usps/imis/errors/response_error_spec.rb +0 -107
  35. data/spec/lib/usps/imis/mapper_spec.rb +0 -55
  36. data/spec/lib/usps/imis/mocks/business_object_spec.rb +0 -65
  37. data/spec/lib/usps/imis/panels/base_panel_spec.rb +0 -33
  38. data/spec/lib/usps/imis/panels/education_spec.rb +0 -70
  39. data/spec/lib/usps/imis/panels/vsc_spec.rb +0 -37
  40. data/spec/lib/usps/imis/properties_spec.rb +0 -19
  41. data/spec/lib/usps/imis_spec.rb +0 -11
  42. data/spec/spec_helper.rb +0 -38
  43. data/usps-imis-api.gemspec +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07d2a2b0c67186b6c3e2a8c8f3dd1694341b399d2ae95bbffcc8db568a3cdca2
4
- data.tar.gz: b6315dd73aff2c70aabf352dfa2b25b905cf11404894a37814a77d7c2c349ef7
3
+ metadata.gz: ebdfba18c4fc6164bd3e5fbf4fe366d2ca81dbe37420bca3035d705b2309319f
4
+ data.tar.gz: 0244bef394a72715ddf7a6e2219f37bcbc93f025ae87c70fe3928fcd45201b0e
5
5
  SHA512:
6
- metadata.gz: 7e4bd50ec6137ffed9a3035d135fbe62d5ec66846e6b11e4b4c3f2db38cb5f20d09f9ca9b41553291a649859dd8aade13318eed85e0d92645e60734545a3f5c2
7
- data.tar.gz: 3bd9f6eb055de1fb5aecbd1c40166f7db9a781cecf5fb93fbe8086636c379c8081d5a97271c5a6f27354439cb7410bf1f13cd7a1c39cb6ccb36ede0ccecf5630
6
+ metadata.gz: cd525cf6c237a7d13addbbd1faeed94b0c8a2cdecc7392d116447e19b91ba69a6d21064c517ce8b1885152eb55d918e129f681238e7b7912d20b0b408a09e441
7
+ data.tar.gz: 7c56a167b87a406f23e89d1c4bc0fb964e19e05b89c36eb180b2deae3f157c47f25c69a8c95cc7eff0f32fb975872edcf4b8cab28ca11516b7b96be430e63e32
data/Readme.md CHANGED
@@ -39,331 +39,9 @@ Usps::Imis.configure do |config|
39
39
  end
40
40
  ```
41
41
 
42
- When using `bin/console`, this configuration will be run by default.
43
-
44
- Instantiate the API object:
45
-
46
- ```ruby
47
- api = Usps::Imis::Api.new
48
- ```
49
-
50
- If you already have an iMIS ID to work with, you can pass that in immediately:
51
-
52
- ```ruby
53
- api = Usps::Imis::Api.new(imis_id: imis_id)
54
- ```
55
-
56
- ### Authentication
57
-
58
- If a token is not available, this will automatically fetch one when needed. As long as that token
59
- should still be valid, it will reuse the same token. If the token should expire, this will
60
- automatically request a new token.
61
-
62
42
  ## Usage
63
43
 
64
- ### iMIS ID
65
-
66
- To act on member data, you need to have the iMIS ID. If you already have access to that from the
67
- database, you can skip this step.
68
-
69
- To convert a member's certificate number into their iMIS ID, run the following method:
70
-
71
- ```ruby
72
- api.imis_id_for(certificate)
73
- ```
74
-
75
- This will both return the ID, and store it for use with other requests. If you need to change which
76
- member you are working with, just run this method again with the new certificate number.
77
-
78
- You can also manually set the current ID, if you already have it for a given member
79
-
80
- ```ruby
81
- api.imis_id = imis_id
82
- ```
83
-
84
- #### Without an iMIS ID
85
-
86
- Running requests without an iMIS ID set will result in query results returned from the API.
87
-
88
- ### Business Object and Panel Actions
89
-
90
- Business Objects and Panels support the following actions.
91
-
92
- Panels require passing in the ordinal identifier as an argument, except for `POST`.
93
-
94
- #### GET
95
-
96
- To fetch member data, run e.g.:
97
-
98
- ```ruby
99
- data = api.on('ABC_ASC_Individual_Demog').get
100
- ```
101
-
102
- You can also pass in specific field names to filter the returned member data, e.g.:
103
-
104
- ```ruby
105
- data = api.on('ABC_ASC_Individual_Demog').get('TotMMS', 'MMS_Updated')
106
- ```
107
-
108
- The response from `get` behaves like a Hash, but directly accesses property values by name.
109
- If you need to access the rest of the underlying data, use the `raw` method:
110
-
111
- ```ruby
112
- data = api.on('ABC_ASC_Individual_Demog').get
113
- data['TotMMS']
114
- data.raw['EntityTypeName']
115
- ```
116
-
117
- Alias: `read`
118
-
119
- #### GET Field
120
-
121
- To fetch a specific field from member data, run e.g.:
122
-
123
- ```ruby
124
- tot_mms = api.on('ABC_ASC_Individual_Demog').get_field('TotMMS')
125
- ```
126
-
127
- You can also access fields directly on the Business Object or Panel like a Hash:
128
-
129
- ```ruby
130
- tot_mms = api.on('ABC_ASC_Individual_Demog')['TotMMS']
131
- ```
132
-
133
- Alias: `fetch`
134
-
135
- #### GET Fields
136
-
137
- To fetch multiple specific fields from member data, run e.g.:
138
-
139
- ```ruby
140
- data = api.on('ABC_ASC_Individual_Demog').get_fields('TotMMS', 'MMS_Updated')
141
- ```
142
-
143
- Alias: `fetch_all`
144
-
145
- #### PUT Fields
146
-
147
- To update member data, run e.g.:
148
-
149
- ```ruby
150
- data = { 'MMS_Updated' => Time.now.strftime('%Y-%m-%dT%H:%M:%S'), 'TotMMS' => new_total }
151
- update = api.on('ABC_ASC_Individual_Demog').put_fields(data)
152
- ```
153
-
154
- This method fetches the current data structure, and filters it down to just what you want to
155
- update, to reduce the likelihood of update collisions or type validation failures.
156
-
157
- Alias: `patch`
158
-
159
- #### PUT
160
-
161
- To update member data, run e.g.:
162
-
163
- ```ruby
164
- update = api.on('ABC_ASC_Individual_Demog').put(complete_imis_object)
165
- ```
166
-
167
- This method requires a complete iMIS data structure. However, any properties not included will be
168
- left unmodified (meaning this also effectively handles `PATCH`, though iMIS does not accept that
169
- HTTP verb).
170
-
171
- Alias: `update`
172
-
173
- #### POST
174
-
175
- To create new member data, run e.g.:
176
-
177
- ```ruby
178
- created = api.on('ABC_ASC_Individual_Demog').post(complete_imis_object)
179
- ```
180
-
181
- This method requires a complete iMIS data structure.
182
-
183
- Alias: `create`
184
-
185
- #### DELETE
186
-
187
- To remove member data, run e.g.:
188
-
189
- ```ruby
190
- api.on('ABC_ASC_Individual_Demog').delete
191
- ```
192
-
193
- Alias: `destroy`
194
-
195
- ### QUERY
196
-
197
- Run an IQA Query
198
-
199
- `query_params` is a hash of shape: `{ param_name => param_value }`
200
-
201
- ```ruby
202
- query = api.query(query_name, query_params)
203
-
204
- query.each do |item|
205
- # Download all pages of the query, then iterate on the results
206
- end
207
-
208
- query.find_each do |item|
209
- # Iterate one page at a time, fetching new pages automatically
210
- end
211
- ```
212
-
213
- ### Field Mapper
214
-
215
- For fields that have already been mapped between the ITCom database and iMIS, you can use the
216
- Mapper class to further simplify the `fetch` / `update` interfaces:
217
-
218
- ```ruby
219
- mm = api.mapper.fetch(:mm)
220
- mm = api.mapper[:mm]
221
- ```
222
-
223
- ```ruby
224
- api.mapper.update(mm: 15)
225
- ```
226
-
227
- For simplicity, you can also call `fetch` (or simply use Hash access syntax) and `update` on the
228
- `Api` class directly:
229
-
230
- ```ruby
231
- api.fetch(:mm)
232
- api[:mm]
233
- ```
234
-
235
- ```ruby
236
- api.update(mm: 15)
237
- api[:mm] = 15
238
- ```
239
-
240
- If there is no known mapping for the requested field, the Mapper will give up, but will provide
241
- you with the standard API call syntax, and will suggest you inform ITCom leadership of the new
242
- mapping you need.
243
-
244
- ### Panels
245
-
246
- For supported panels (usually, business objects with composite identity keys), you can interact
247
- with them in the same general way:
248
-
249
- ```ruby
250
- vsc = Usps::Imis::Panels::Vsc.new(imis_id: 6374)
251
-
252
- vsc.get(1417)
253
-
254
- # All of these options are identical
255
- #
256
- vsc.get(1417, 'Quantity').first
257
- vsc.get(1417)['Quantity']
258
- vsc[1417, 'Quantity']
259
- vsc.get(1417).raw['Properties']['$values'].find { it['Name'] == 'Quantity' }['Value']['$value']
260
- vsc.get_field(1417, 'Quantity')
261
-
262
- created = vsc.create(certificate: 'E136924', year: 2024, count: 42)
263
-
264
- # Get the Ordinal identifier from the response
265
- #
266
- # All of these options are identical
267
- #
268
- ordinal = created.ordinal
269
- ordinal = created['Ordinal']
270
- ordinal = created.raw['Properties']['$values'].find { it['Name'] == 'Ordinal' }['Value']['$value']
271
- ordinal = created.raw['Identity']['IdentityElements']['$values'][1].to_i # Value is duplicated here
272
-
273
- vsc.update(certificate: 'E136924', year: 2024, count: 43, ordinal: ordinal)
274
-
275
- vsc.put_fields(ordinal, 'Quantity' => 44)
276
- vsc['Quantity'] = 44
277
-
278
- vsc.destroy(ordinal)
279
- ```
280
-
281
- If you already have an iMIS ID to work with, you can pass that in immediately:
282
-
283
- ```ruby
284
- vsc = Usps::Imis::Panels::Vsc.new(imis_id: imis_id)
285
- ```
286
-
287
- Panels are also accessible directly from the API object:
288
-
289
- ```ruby
290
- api.panels.vsc.get(1417)
291
- ```
292
-
293
- ### DSL Mode
294
-
295
- Instead of manually setting the current iMIS ID, then running individual queries, you can instead
296
- run queries in DSL mode. This specifies the iMIS ID for the scope of the block, then reverts to the
297
- previous value.
298
-
299
- ```ruby
300
- api.with(31092) do
301
- # These requests are identical:
302
-
303
- on('ABC_ASC_Individual_Demog') { put_fields('TotMMS' => 15) }
304
-
305
- on('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15)
306
-
307
- mapper.update(mm: 15)
308
-
309
- update(mm: 15)
310
-
311
- mapper[:mm] = 15
312
- end
313
-
314
- # This request fetches the same data, but leaves the iMIS ID selected
315
- api.with(31092)[:mm] = 15
316
- ```
317
-
318
- ```ruby
319
- api.with(6374) do
320
- panels.vsc.get(1417)
321
- end
322
- ```
323
-
324
- ```ruby
325
- api.with(31092) do
326
- # These requests are identical:
327
-
328
- on('ABC_ASC_Individual_Demog') do
329
- get.raw['Properties']['$values'].find { it['Name'] == 'TotMMS' }['Value']['$value']
330
-
331
- get['TotMMS']
332
-
333
- get_field('TotMMS')
334
-
335
- get_fields('TotMMS').first
336
- end
337
-
338
- on('ABC_ASC_Individual_Demog').get_field('TotMMS')
339
-
340
- on('ABC_ASC_Individual_Demog')['TotMMS']
341
- end
342
-
343
- # These requests fetch the same data, but leave the iMIS ID selected
344
- api.with(31092).on('ABC_ASC_Individual_Demog').get_field('TotMMS')
345
- api.with(31092).on('ABC_ASC_Individual_Demog')['TotMMS']
346
- ```
347
-
348
- ### Data Methods
349
-
350
- Data responses from the API can be handled as a standard Hash using the `raw` method.
351
-
352
- If you need to access all of the property values, you can use the `properties` method.
353
- By default, this will exclude the `ID` and `Ordinal` properties; they can be included with
354
- `properties(include_ids: true)`.
355
-
356
- ## Test Data Mocking
357
-
358
- You can use the provided Business Object Mock to generate stub data for rspec:
359
-
360
- ```ruby
361
- allow(api).to(
362
- receive(:on).with('ABC_ASC_Individual_Demog').and_return(
363
- Usps::Imis::Mocks::BusinessObject.new(TotMMS: 2)
364
- )
365
- )
366
- ```
44
+ For more details and examples, refer to the [Wiki](https://github.com/unitedstatespowersquadrons/imis-api-ruby/wiki).
367
45
 
368
46
  ## Exception Handling
369
47
 
@@ -377,6 +55,8 @@ Testing is available by running:
377
55
  bundle exec rspec
378
56
  ```
379
57
 
58
+ API web requests are sanitized and recorded using VCR.
59
+
380
60
  Linting is available by running:
381
61
 
382
62
  ```ruby
data/lib/usps/imis/api.rb CHANGED
@@ -36,11 +36,9 @@ module Usps
36
36
 
37
37
  # A new instance of +Api+
38
38
  #
39
- # @param skip_authentication [bool] Skip authentication on initialization (used for tests)
40
39
  # @param imis_id [Integer, String] iMIS ID to select immediately on initialization
41
40
  #
42
- def initialize(skip_authentication: false, imis_id: nil)
43
- authenticate unless skip_authentication
41
+ def initialize(imis_id: nil)
44
42
  self.imis_id = imis_id if imis_id
45
43
  end
46
44
 
@@ -63,8 +61,12 @@ module Usps
63
61
  def imis_id_for(certificate)
64
62
  raise Errors::LockedIdError if lock_imis_id
65
63
 
64
+ logger.debug "Fetching iMIS ID for #{certificate}"
65
+
66
66
  begin
67
- self.imis_id = query(Imis.configuration.imis_id_query_name, { certificate: }).first['ID'].to_i
67
+ result = query(Imis.configuration.imis_id_query_name, { certificate: }).tap { logger.debug it }
68
+ page = result.page.tap { logger.debug it }
69
+ self.imis_id = page.first['ID'].to_i
68
70
  rescue StandardError
69
71
  raise Errors::NotFoundError, 'Member not found'
70
72
  end
@@ -97,16 +99,16 @@ module Usps
97
99
  end
98
100
  end
99
101
 
100
- # Build an IQA Query interface
102
+ # Build a Query interface
103
+ #
104
+ # Works with IQA queries and Business Objects
101
105
  #
102
- # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
106
+ # @param query_name [String] Full path of the query, e.g. +$/_ABC/Fiander/iMIS_ID+
103
107
  # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
104
108
  #
105
- # @return [Hash] Response data from the API
109
+ # @return [Usps::Imis::Query] Query wrapper
106
110
  #
107
- def query(query_name, query_params = {})
108
- Query.new(self, query_name, query_params)
109
- end
111
+ def query(query_name, query_params = {}) = Query.new(self, query_name, **query_params)
110
112
 
111
113
  # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
112
114
  #
@@ -162,20 +164,22 @@ module Usps
162
164
  def authenticate
163
165
  logger.debug 'Authenticating with iMIS'
164
166
 
165
- uri = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
166
- req = Net::HTTP::Post.new(uri)
167
- authentication_data = {
167
+ request = http_post
168
+ request.body = URI.encode_www_form(
168
169
  grant_type: 'password',
169
170
  username: Imis.configuration.username,
170
171
  password: Imis.configuration.password
171
- }
172
- req.body = URI.encode_www_form(authentication_data)
173
- result = submit(uri, req)
172
+ )
173
+ result = submit(uri, request)
174
174
  json = JSON.parse(result.body)
175
175
 
176
176
  @token = json['access_token']
177
- @token_expiration = Time.parse(json['.expires'])
177
+ @token_expiration = Time.now - json['expires_in'] - 60
178
178
  end
179
+
180
+ # URI for the authentication endpoint
181
+ #
182
+ def uri(...) = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
179
183
  end
180
184
  end
181
185
  end
@@ -33,6 +33,12 @@ module Usps
33
33
  @ordinal = ordinal
34
34
  end
35
35
 
36
+ # Run a query on the entire business object
37
+ #
38
+ # @return [Usps::Imis::Query] Query wrapper
39
+ #
40
+ def query = api.query(business_object_name)
41
+
36
42
  # Get a business object for the current member
37
43
  #
38
44
  # If +fields+ is provided, will return only those field values
@@ -93,7 +99,7 @@ module Usps
93
99
  #
94
100
  # @return [Usps::Imis::Data] Response data from the API
95
101
  #
96
- def put(body) = put_object(Net::HTTP::Put.new(uri), body)
102
+ def put(body) = put_object(http_put, body)
97
103
  alias update put
98
104
 
99
105
  # Create a business object for the current member
@@ -102,14 +108,14 @@ module Usps
102
108
  #
103
109
  # @return [Usps::Imis::Data] Response data from the API
104
110
  #
105
- def post(body) = put_object(Net::HTTP::Post.new(uri(id: '')), body)
111
+ def post(body) = put_object(http_post, body)
106
112
  alias create post
107
113
 
108
114
  # Remove a business object for the current member
109
115
  #
110
116
  # @return [true] Only on success response (i.e. blank string from the API)
111
117
  #
112
- def delete = submit(uri, authorize(Net::HTTP::Delete.new(uri))).body == '' # rubocop:disable Naming/PredicateMethod
118
+ def delete = submit(uri, authorize(http_delete)).body == '' # rubocop:disable Naming/PredicateMethod
113
119
  alias destroy delete
114
120
 
115
121
  # Ruby 3.5 instance variable filter
@@ -118,9 +124,6 @@ module Usps
118
124
 
119
125
  private
120
126
 
121
- def token = api.token
122
- def token_expiration = api.token_expiration
123
-
124
127
  def logger = Imis.logger('BusinessObject')
125
128
 
126
129
  # Construct a business object API endpoint address
@@ -154,13 +157,7 @@ module Usps
154
157
  # Skip unmodified fields
155
158
  next unless fields.keys.include?(value['Name'])
156
159
 
157
- # Strings are not wrapped in the type definition structure
158
- new_value = fields[value['Name']]
159
- if new_value.is_a?(String)
160
- value['Value'] = new_value
161
- else
162
- value['Value']['$value'] = new_value
163
- end
160
+ value['Value'] = Properties.wrap(fields[value['Name']])
164
161
 
165
162
  # Add the completed field with the updated value
166
163
  updated['Properties']['$values'] << value
@@ -173,9 +170,10 @@ module Usps
173
170
  # Useful for stubbing data in tests
174
171
  #
175
172
  def raw_object
176
- request = Net::HTTP::Get.new(uri)
177
- result = submit(uri, authorize(request))
178
- result = Data.from_json(result.body)
173
+ raise Errors::MissingIdError if api.imis_id.nil?
174
+
175
+ response = submit(uri, authorize(http_get))
176
+ result = Data.from_json(response.body)
179
177
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
180
178
  result
181
179
  end
@@ -183,9 +181,11 @@ module Usps
183
181
  # Upload an object to the API
184
182
  #
185
183
  def put_object(request, body)
184
+ raise Errors::MissingIdError if api.imis_id.nil?
185
+
186
186
  request.body = JSON.dump(body)
187
- result = submit(uri, authorize(request))
188
- result = Data.from_json(result.body)
187
+ response = submit(uri, authorize(request))
188
+ result = Data.from_json(response.body)
189
189
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
190
190
  result
191
191
  end
@@ -12,7 +12,7 @@ module Usps
12
12
  attr_reader :environment, :logger, :logger_level
13
13
 
14
14
  def initialize
15
- @environment = defined?(Rails) ? Rails.env : ActiveSupport::StringInquirer.new('development')
15
+ @environment = defined?(::Rails) ? ::Rails.env : ActiveSupport::StringInquirer.new('development')
16
16
  @imis_id_query_name = ENV.fetch('IMIS_ID_QUERY_NAME', nil)
17
17
  @username = ENV.fetch('IMIS_USERNAME', nil)
18
18
  @password = ENV.fetch('IMIS_PASSWORD', nil)
@@ -31,6 +31,10 @@ module Usps
31
31
  @logger = ActiveSupport::TaggedLogging.new(logger)
32
32
  end
33
33
 
34
+ def silence!
35
+ self.logger = Logger.new(nil)
36
+ end
37
+
34
38
  # Environment-specific API endpoint hostname
35
39
  #
36
40
  # @return The API hostname for the current environment
@@ -44,7 +48,7 @@ module Usps
44
48
 
45
49
  # Ruby 3.5 instance variable filter
46
50
  #
47
- def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username @logger_level]
51
+ def instance_variables_to_inspect = instance_variables - %i[@password @logger]
48
52
 
49
53
  # Parameters to filter out of logging
50
54
  #
@@ -50,6 +50,10 @@ module Usps
50
50
  .index_with { self[it] }
51
51
  end
52
52
 
53
+ def []=(...)
54
+ raise Errors::ApiError, '`Data` does not support setting values. If you need to modify it, call `.raw` on it.'
55
+ end
56
+
53
57
  def inspect
54
58
  stringio = StringIO.new
55
59
  PP.pp(self, stringio)
@@ -18,7 +18,7 @@ module Usps
18
18
  super(message)
19
19
  @metadata = metadata
20
20
 
21
- Imis.logger.debug self
21
+ Imis.logger(self.class.name).error self
22
22
  end
23
23
 
24
24
  # Additional metadata to include in Bugsnag reports
@@ -47,6 +47,7 @@ require_relative 'errors/api_error'
47
47
  require_relative 'errors/config_error'
48
48
  require_relative 'errors/locked_id_error'
49
49
  require_relative 'errors/mapper_error'
50
+ require_relative 'errors/missing_id_error'
50
51
  require_relative 'errors/not_found_error'
51
52
  require_relative 'errors/response_error'
52
53
  require_relative 'errors/panel_unimplemented_error'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Errors
6
+ # Exception raised when attempting to access a +BusinessObject+ without an iMIS ID
7
+ #
8
+ class MissingIdError < Error
9
+ def initialize = super(message)
10
+
11
+ def message = 'Cannot access an individual Business Object without an iMIS ID'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -51,12 +51,13 @@ module Usps
51
51
 
52
52
  # Update a single named field on a business object for the current member
53
53
  #
54
+ # @param ordinal [Integer] The ordinal identifier for the desired object
54
55
  # @param field [String] Name of the field
55
56
  # @param value Value of the field
56
57
  #
57
58
  # @return [Usps::Imis::Data] Response data from the API
58
59
  #
59
- def put_field(field, value) = api.on(business_object, ordinal:).put_field(field, value)
60
+ def put_field(ordinal, field, value) = api.on(business_object, ordinal:).put_field(field, value)
60
61
  alias []= put_field
61
62
 
62
63
  # Update only specific fields on a Panel for the current member
@@ -9,6 +9,19 @@ module Usps
9
9
  #
10
10
  def self.build(&) = new.build(&)
11
11
 
12
+ # Wrap value in the API-internal type structure
13
+ #
14
+ def self.wrap(value)
15
+ case value
16
+ when String then value
17
+ when Time, DateTime then value.strftime('%Y-%m-%dT%H:%I:%S')
18
+ when Integer then { '$type' => 'System.Int32', '$value' => value }
19
+ when true, false then { '$type' => 'System.Boolean', '$value' => value }
20
+ else
21
+ raise Errors::UnexpectedPropertyTypeError.from(value)
22
+ end
23
+ end
24
+
12
25
  # Build the data for the Properties field
13
26
  #
14
27
  def build
@@ -29,22 +42,9 @@ module Usps
29
42
  @properties << {
30
43
  '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
31
44
  'Name' => name,
32
- 'Value' => wrap(value)
45
+ 'Value' => self.class.wrap(value)
33
46
  }
34
47
  end
35
-
36
- private
37
-
38
- def wrap(value)
39
- case value
40
- when String then value
41
- when Time, DateTime then value.strftime('%Y-%m-%dT%H:%I:%S')
42
- when Integer then { '$type' => 'System.Int32', '$value' => value }
43
- when true, false then { '$type' => 'System.Boolean', '$value' => value }
44
- else
45
- raise Errors::UnexpectedPropertyTypeError.from(value)
46
- end
47
- end
48
48
  end
49
49
  end
50
50
  end