usps-imis-api 1.0.0.pre.rc.9 → 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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +1 -323
  3. data/lib/usps/imis/api.rb +11 -6
  4. data/lib/usps/imis/business_object.rb +11 -10
  5. data/lib/usps/imis/config.rb +6 -2
  6. data/lib/usps/imis/error.rb +1 -0
  7. data/lib/usps/imis/errors/missing_id_error.rb +15 -0
  8. data/lib/usps/imis/panels/base_panel.rb +2 -1
  9. data/lib/usps/imis/properties.rb +14 -14
  10. data/lib/usps/imis/query.rb +44 -23
  11. data/lib/usps/imis/requests.rb +5 -1
  12. data/lib/usps/imis/version.rb +1 -1
  13. data/spec/support/usps/vcr/config.rb +47 -0
  14. data/spec/support/usps/vcr/filters.rb +89 -0
  15. data/spec/support/usps/vcr.rb +8 -0
  16. metadata +6 -70
  17. data/.github/workflows/main.yml +0 -57
  18. data/.gitignore +0 -5
  19. data/.rspec +0 -2
  20. data/.rubocop.yml +0 -89
  21. data/.ruby-version +0 -1
  22. data/.simplecov +0 -8
  23. data/Gemfile +0 -16
  24. data/Gemfile.lock +0 -147
  25. data/Rakefile +0 -12
  26. data/bin/console +0 -21
  27. data/bin/setup +0 -8
  28. data/spec/fixtures/vcr_cassettes/.keep +0 -0
  29. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_authorize/automatically_refreshes_an_expired_token.yml +0 -67
  30. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_imis_id_for/gets_the_iMIS_ID.yml +0 -131
  31. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_imis_id_for/with_a_query_error/wraps_errors.yml +0 -67
  32. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_inspect/does_not_show_the_token_instance_variable.yml +0 -67
  33. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_inspect/is_configured_to_exclude_the_token_instance_variable.yml +0 -67
  34. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/chains_with_on_to_a_single_block.yml +0 -256
  35. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/nests_on_and_with.yml +0 -256
  36. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/returns_a_BusinessObject_without_a_block.yml +0 -67
  37. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/sends_an_update_from_put.yml +0 -256
  38. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_put/sends_an_update.yml +0 -256
  39. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_put/when_receiving_a_response_error/wraps_the_error.yml +0 -67
  40. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_query/collects_all_query_results.yml +0 -67
  41. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/blocks_calling_imis_id_.yml +0 -67
  42. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/blocks_calling_imis_id_for.yml +0 -67
  43. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/sends_an_update_from_put.yml +0 -256
  44. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/sends_an_update_from_update.yml +0 -256
  45. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/uses_a_panel_correctly.yml +0 -145
  46. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/stores_the_initial_imis_id.yml +0 -67
  47. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/_put_field/submits_the_correct_update_request.yml +0 -627
  48. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_filter_fields/formats_fields_correctly.yml +0 -67
  49. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get/delegation_to_get_fields/delegates_to_get_fields.yml +0 -67
  50. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get/returns_multiple_values.yml +0 -67
  51. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_field/returns_a_string_value.yml +0 -67
  52. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_field/returns_an_integer_value.yml +0 -67
  53. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_fields/returns_multiple_values.yml +0 -67
  54. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/fetches_a_mapped_field.yml +0 -158
  55. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/raises_for_unmapped_updates.yml +0 -67
  56. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/supports_Hash_access_syntax.yml +0 -158
  57. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/supports_Hash_access_syntax_on_the_Api_directly.yml +0 -158
  58. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_update/raises_for_unmapped_updates.yml +0 -67
  59. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_update/sends_a_mapped_update.yml +0 -256
  60. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/initialize_with_imis_id/stores_the_initial_imis_id.yml +0 -67
  61. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_BasePanel/requires_business_object_to_be_defined.yml +0 -67
  62. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_BasePanel/requires_payload_data_to_be_defined.yml +0 -67
  63. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get/loads_a_specific_object.yml +0 -146
  64. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get/returns_specific_fields.yml +0 -146
  65. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get_field/returns_a_specific_field.yml +0 -146
  66. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get_fields/returns_specific_fields.yml +0 -146
  67. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/interacts_with_records_correctly.yml +0 -495
  68. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/initialization_with_ID/can_initialize_with_an_iMIS_ID.yml +0 -67
  69. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Vsc/_get/loads_a_specific_object.yml +0 -145
  70. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Vsc/handles_new_records_correctly.yml +0 -319
  71. data/spec/lib/usps/imis/api_spec.rb +0 -161
  72. data/spec/lib/usps/imis/business_object_spec.rb +0 -87
  73. data/spec/lib/usps/imis/config_spec.rb +0 -59
  74. data/spec/lib/usps/imis/data_spec.rb +0 -75
  75. data/spec/lib/usps/imis/error_spec.rb +0 -17
  76. data/spec/lib/usps/imis/errors/response_error_spec.rb +0 -107
  77. data/spec/lib/usps/imis/mapper_spec.rb +0 -55
  78. data/spec/lib/usps/imis/mocks/business_object_spec.rb +0 -65
  79. data/spec/lib/usps/imis/panels/base_panel_spec.rb +0 -33
  80. data/spec/lib/usps/imis/panels/education_spec.rb +0 -70
  81. data/spec/lib/usps/imis/panels/vsc_spec.rb +0 -37
  82. data/spec/lib/usps/imis/properties_spec.rb +0 -19
  83. data/spec/lib/usps/imis_spec.rb +0 -11
  84. data/spec/spec_helper.rb +0 -78
  85. data/usps-imis-api.gemspec +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f59d7505f179cc5306b35b07abb68c618229101a28a637240299dfffce94bec7
4
- data.tar.gz: 6b0f55756ea84ee927b6a7115aef6fdd96934dc2b9256c0cc6b2beac9be8c849
3
+ metadata.gz: ebdfba18c4fc6164bd3e5fbf4fe366d2ca81dbe37420bca3035d705b2309319f
4
+ data.tar.gz: 0244bef394a72715ddf7a6e2219f37bcbc93f025ae87c70fe3928fcd45201b0e
5
5
  SHA512:
6
- metadata.gz: b83571a81496bf22fcea8886c82aeba87eb5f9a687f7eb3e033458f89bfd0ac58530e72de9cc1a97f315bf6721e1e58dfa7fd527391188ad3ef8c371fdc5dbbd
7
- data.tar.gz: 232d44e8916a5fa4166a5818609595ba217fec1e61438e175161d0d39e52d16a537457b3829ff673dc0b4b7adfa1eec2c46f5056eb6eb50e8c2a6864abfbe53c
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
 
data/lib/usps/imis/api.rb CHANGED
@@ -39,7 +39,6 @@ module Usps
39
39
  # @param imis_id [Integer, String] iMIS ID to select immediately on initialization
40
40
  #
41
41
  def initialize(imis_id: nil)
42
- authenticate
43
42
  self.imis_id = imis_id if imis_id
44
43
  end
45
44
 
@@ -62,8 +61,12 @@ module Usps
62
61
  def imis_id_for(certificate)
63
62
  raise Errors::LockedIdError if lock_imis_id
64
63
 
64
+ logger.debug "Fetching iMIS ID for #{certificate}"
65
+
65
66
  begin
66
- 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
67
70
  rescue StandardError
68
71
  raise Errors::NotFoundError, 'Member not found'
69
72
  end
@@ -96,12 +99,14 @@ module Usps
96
99
  end
97
100
  end
98
101
 
99
- # Build an IQA Query interface
102
+ # Build a Query interface
103
+ #
104
+ # Works with IQA queries and Business Objects
100
105
  #
101
- # @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+
102
107
  # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
103
108
  #
104
- # @return [Hash] Response data from the API
109
+ # @return [Usps::Imis::Query] Query wrapper
105
110
  #
106
111
  def query(query_name, query_params = {}) = Query.new(self, query_name, **query_params)
107
112
 
@@ -169,7 +174,7 @@ module Usps
169
174
  json = JSON.parse(result.body)
170
175
 
171
176
  @token = json['access_token']
172
- @token_expiration = Time.parse(json['.expires'])
177
+ @token_expiration = Time.now - json['expires_in'] - 60
173
178
  end
174
179
 
175
180
  # URI for the authentication endpoint
@@ -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
@@ -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,6 +170,8 @@ module Usps
173
170
  # Useful for stubbing data in tests
174
171
  #
175
172
  def raw_object
173
+ raise Errors::MissingIdError if api.imis_id.nil?
174
+
176
175
  response = submit(uri, authorize(http_get))
177
176
  result = Data.from_json(response.body)
178
177
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
@@ -182,6 +181,8 @@ module Usps
182
181
  # Upload an object to the API
183
182
  #
184
183
  def put_object(request, body)
184
+ raise Errors::MissingIdError if api.imis_id.nil?
185
+
185
186
  request.body = JSON.dump(body)
186
187
  response = submit(uri, authorize(request))
187
188
  result = Data.from_json(response.body)
@@ -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
  #
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- # API wrapper for IQA Queries
5
+ # API wrapper for IQA and Business Object Queries
6
6
  #
7
7
  class Query
8
8
  include Enumerable
@@ -10,7 +10,7 @@ module Usps
10
10
 
11
11
  # Endpoint for IQA query requests
12
12
  #
13
- QUERY_PATH = 'api/Query'
13
+ IQA_PATH = 'api/Query'
14
14
 
15
15
  # The parent +Api+ object
16
16
  #
@@ -36,6 +36,10 @@ module Usps
36
36
  #
37
37
  attr_reader :count
38
38
 
39
+ # Whether the current query has a next page
40
+ #
41
+ attr_reader :next_page
42
+
39
43
  # A new instance of +Query+
40
44
  #
41
45
  # @param api [Api] Parent to use for making requests
@@ -48,14 +52,18 @@ module Usps
48
52
  @api = api
49
53
  @query_name = query_name
50
54
  @query_params = query_params
55
+ @query_params.merge!(QueryName: query_name) if iqa?
51
56
  @page_size = page_size
52
57
  @offset = offset
58
+ @count = 0
59
+
60
+ logger.debug "URI: #{uri}"
53
61
  end
54
62
 
55
63
  # Iterate through all results from the query
56
64
  #
57
65
  def each(&)
58
- logger.info 'Running IQA Query on iMIS'
66
+ logger.info 'Running'
59
67
 
60
68
  items = []
61
69
  find_each { items << it }
@@ -65,10 +73,10 @@ module Usps
65
73
  # Iterate through all results from the query, fetching one page at a time
66
74
  #
67
75
  def find_each(&)
68
- result = reset!
76
+ reset!
69
77
 
70
- while result['HasNext']
71
- result = fetch_next.tap do |result_page|
78
+ while page?
79
+ fetch_next.tap do |result_page|
72
80
  result_page['Items']['$values'].map { it.except('$type') }.each(&)
73
81
  end
74
82
  end
@@ -76,14 +84,16 @@ module Usps
76
84
  nil
77
85
  end
78
86
 
79
- # Fetch a raw query page
87
+ # Fetch a filtered query page, and update the current offset
80
88
  #
81
- def fetch = JSON.parse(submit(uri, authorize(http_get)).body)
89
+ def page = fetch_next['Items']['$values'].map { iqa? ? it.except('$type') : Imis::Data[it] }
82
90
 
83
91
  # Fetch the next raw query page, and update the current offset
84
92
  #
85
93
  def fetch_next
86
- logger.info 'Fetching IQA Query page'
94
+ return unless page?
95
+
96
+ logger.info "Fetching #{query_type} Query page"
87
97
 
88
98
  result = fetch
89
99
 
@@ -94,33 +104,44 @@ module Usps
94
104
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
95
105
 
96
106
  @offset = result['NextOffset']
107
+ @next_page = result['HasNext']
97
108
 
98
109
  result
99
110
  end
100
111
 
112
+ # Fetch a raw query page
113
+ #
114
+ def fetch = JSON.parse(submit(uri, authorize(http_get)).body)
115
+
116
+ # Reset query paging progress
117
+ #
118
+ def reset!
119
+ return if next_page.nil?
120
+
121
+ logger.debug 'Resetting progress'
122
+
123
+ @count = 0
124
+ @offset = 0
125
+ @next_page = nil
126
+ end
127
+
101
128
  # Ruby 3.5 instance variable filter
102
129
  #
103
130
  def instance_variables_to_inspect = instance_variables - %i[@api]
104
131
 
105
132
  private
106
133
 
107
- def token = api.token
108
- def token_expiration = api.token_expiration
134
+ # Only skip if explicitly set
135
+ def page? = next_page.nil? || next_page
109
136
 
110
- def path_params = query_params.merge(QueryName: query_name).merge({ Offset: offset, Limit: page_size }.compact)
111
- def path = "#{QUERY_PATH}?#{path_params.to_query}"
137
+ def iqa? = query_name.match?(/^\$/)
138
+ def query_type = iqa? ? :IQA : :'Business Object'
139
+ def path_params = query_params.merge({ Offset: offset, Limit: page_size }.compact)
140
+ def endpoint = iqa? ? IQA_PATH : "#{Imis::BusinessObject::API_PATH}/#{query_name}"
141
+ def path = "#{endpoint}?#{path_params.to_query}"
112
142
  def uri = URI(File.join(Imis.configuration.hostname, path))
113
143
 
114
- def logger = Imis.logger('Query')
115
-
116
- def reset!
117
- logger.debug 'Resetting Query progress'
118
-
119
- @count = 0
120
- @offset = 0
121
-
122
- { 'HasNext' => true }
123
- end
144
+ def logger = Imis.logger("#{query_type} Query")
124
145
  end
125
146
  end
126
147
  end