usps-imis-api 0.9.7 → 0.9.9

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +20 -2
  5. data/Readme.md +12 -2
  6. data/lib/usps/imis/api.rb +16 -12
  7. data/lib/usps/imis/business_object.rb +17 -8
  8. data/lib/usps/imis/data.rb +4 -0
  9. data/lib/usps/imis/error.rb +2 -0
  10. data/lib/usps/imis/mapper.rb +23 -0
  11. data/lib/usps/imis/panels/base_panel.rb +10 -0
  12. data/lib/usps/imis/query.rb +50 -18
  13. data/lib/usps/imis/requests.rb +7 -3
  14. data/lib/usps/imis/version.rb +1 -1
  15. data/spec/fixtures/vcr_cassettes/.keep +0 -0
  16. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_authorize/automatically_refreshes_an_expired_token.yml +67 -0
  17. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_imis_id_for/gets_the_iMIS_ID.yml +131 -0
  18. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_imis_id_for/with_a_query_error/wraps_errors.yml +67 -0
  19. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_inspect/does_not_show_the_token_instance_variable.yml +67 -0
  20. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_inspect/is_configured_to_exclude_the_token_instance_variable.yml +67 -0
  21. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/chains_with_on_to_a_single_block.yml +256 -0
  22. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/nests_on_and_with.yml +256 -0
  23. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/returns_a_BusinessObject_without_a_block.yml +67 -0
  24. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_on/sends_an_update_from_put.yml +256 -0
  25. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_put/sends_an_update.yml +256 -0
  26. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_put/when_receiving_a_response_error/wraps_the_error.yml +67 -0
  27. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_query/collects_all_query_results.yml +67 -0
  28. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/blocks_calling_imis_id_.yml +67 -0
  29. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/blocks_calling_imis_id_for.yml +67 -0
  30. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/sends_an_update_from_put.yml +256 -0
  31. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/sends_an_update_from_update.yml +256 -0
  32. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/_with/uses_a_panel_correctly.yml +145 -0
  33. data/spec/fixtures/vcr_cassettes/Usps_Imis_Api/stores_the_initial_imis_id.yml +67 -0
  34. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/_put_field/submits_the_correct_update_request.yml +627 -0
  35. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_filter_fields/formats_fields_correctly.yml +67 -0
  36. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get/delegation_to_get_fields/delegates_to_get_fields.yml +67 -0
  37. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get/returns_multiple_values.yml +67 -0
  38. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_field/returns_a_string_value.yml +67 -0
  39. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_field/returns_an_integer_value.yml +67 -0
  40. data/spec/fixtures/vcr_cassettes/Usps_Imis_BusinessObject/with_stubbed_data/_get_fields/returns_multiple_values.yml +67 -0
  41. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/fetches_a_mapped_field.yml +158 -0
  42. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/raises_for_unmapped_updates.yml +67 -0
  43. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/supports_Hash_access_syntax.yml +158 -0
  44. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_fetch/supports_Hash_access_syntax_on_the_Api_directly.yml +158 -0
  45. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_update/raises_for_unmapped_updates.yml +67 -0
  46. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/_update/sends_a_mapped_update.yml +256 -0
  47. data/spec/fixtures/vcr_cassettes/Usps_Imis_Mapper/initialize_with_imis_id/stores_the_initial_imis_id.yml +67 -0
  48. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_BasePanel/requires_business_object_to_be_defined.yml +67 -0
  49. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_BasePanel/requires_payload_data_to_be_defined.yml +67 -0
  50. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get/loads_a_specific_object.yml +146 -0
  51. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get/returns_specific_fields.yml +146 -0
  52. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get_field/returns_a_specific_field.yml +146 -0
  53. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/_get_fields/returns_specific_fields.yml +146 -0
  54. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/api_example/interacts_with_records_correctly.yml +495 -0
  55. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Education/initialization_with_ID/can_initialize_with_an_iMIS_ID.yml +67 -0
  56. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Vsc/_get/loads_a_specific_object.yml +145 -0
  57. data/spec/fixtures/vcr_cassettes/Usps_Imis_Panels_Vsc/handles_new_records_correctly.yml +319 -0
  58. data/spec/lib/usps/imis/api_spec.rb +4 -14
  59. data/spec/lib/usps/imis/business_object_spec.rb +63 -50
  60. data/spec/lib/usps/imis/data_spec.rb +9 -0
  61. data/spec/lib/usps/imis/mapper_spec.rb +1 -1
  62. data/spec/lib/usps/imis/panels/base_panel_spec.rb +1 -1
  63. data/spec/lib/usps/imis/panels/education_spec.rb +1 -1
  64. data/spec/lib/usps/imis/panels/vsc_spec.rb +1 -1
  65. data/spec/spec_helper.rb +40 -0
  66. metadata +44 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a78cd8daed5969574e8ec5fc3120a8a5a6c362e519918620ce41c2ed0fc0f26c
4
- data.tar.gz: 0f20aa470ec2f2b8501f670046ee1071fea8552c22dd9f70165fb2ea986a46aa
3
+ metadata.gz: cc0ebcfc78fa3368d5901ad0e9bf03b6149ec0b2e84fecbb25e2449ba866fe09
4
+ data.tar.gz: 04a02698dd0936b051086fc57714a00d6fe90150f82002f3a944a47189ecd120
5
5
  SHA512:
6
- metadata.gz: 41f2a2c9082ce49399a1a9b3e44ddd2cb86a8624d112ea6c36875e05da74db61cd6272f96819d0d889c5ac83e857f900ecaad357da9dd25a3b3094b7354dbce4
7
- data.tar.gz: 131e53225ae3d111b49c64302a99d5b32227e45b2734c42ea2131d5de986c60c2d359c43aef10a5191ce03d6406c5f6bbe0fcfadd11843347533cef0dcd555f3
6
+ metadata.gz: 60cb55c6754bf92f8ddb804726d6dc0272bfd09c09afb912dc9e5d0fd6e0b7b4984cfe1ea9761b22c38e3a855f0b7675c492c9cb811b9afc2a1e742ebdf2fe49
7
+ data.tar.gz: f7824ff8fae0a47456811457ead9f91d2bd8c6a481f9b70342bb76cb66b29f93b60ce03980ea339f56baf5099e0b250f10b9f7990be0f0bc30535be3e109d76c
data/.rubocop.yml CHANGED
@@ -84,3 +84,6 @@ Security/JSONLoad:
84
84
  Enabled: true
85
85
  Security/YAMLLoad:
86
86
  Enabled: true
87
+
88
+ RSpec/NestedGroups:
89
+ Max: 4
data/Gemfile CHANGED
@@ -10,3 +10,7 @@ gem 'rspec', '>= 3.13.0'
10
10
  gem 'rubocop', '>= 1.66.1'
11
11
  gem 'rubocop-rspec', '>= 3.1.0'
12
12
  gem 'simplecov', '>= 0.22.0'
13
+
14
+ gem 'cgi', '>= 0.5.0' # Required to resolve a loading issue for VCR
15
+ gem 'vcr', '>= 6.3.1'
16
+ gem 'webmock', '>= 3.25.2'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- usps-imis-api (0.9.7)
4
+ usps-imis-api (0.9.9)
5
5
  activesupport (~> 8.0)
6
6
 
7
7
  GEM
@@ -20,17 +20,24 @@ GEM
20
20
  securerandom (>= 0.3)
21
21
  tzinfo (~> 2.0, >= 2.0.5)
22
22
  uri (>= 0.13.1)
23
+ addressable (2.8.7)
24
+ public_suffix (>= 2.0.2, < 7.0)
23
25
  ast (2.4.3)
24
26
  base64 (0.3.0)
25
27
  bigdecimal (3.3.1)
28
+ cgi (0.5.0)
26
29
  concurrent-ruby (1.3.5)
27
30
  connection_pool (2.5.4)
31
+ crack (1.0.1)
32
+ bigdecimal
33
+ rexml
28
34
  date (3.4.1)
29
35
  diff-lcs (1.6.2)
30
36
  docile (1.4.1)
31
37
  dotenv (3.1.8)
32
38
  drb (2.2.3)
33
39
  erb (5.1.1)
40
+ hashdiff (1.2.1)
34
41
  i18n (1.14.7)
35
42
  concurrent-ruby (~> 1.0)
36
43
  io-console (0.8.1)
@@ -38,7 +45,7 @@ GEM
38
45
  pp (>= 0.6.0)
39
46
  rdoc (>= 4.0.0)
40
47
  reline (>= 0.4.2)
41
- json (2.15.1)
48
+ json (2.15.2)
42
49
  language_server-protocol (3.17.0.5)
43
50
  lint_roller (1.1.0)
44
51
  logger (1.7.0)
@@ -54,6 +61,7 @@ GEM
54
61
  psych (5.2.6)
55
62
  date
56
63
  stringio
64
+ public_suffix (6.0.2)
57
65
  racc (1.8.1)
58
66
  rainbow (3.1.1)
59
67
  rake (13.3.0)
@@ -64,6 +72,7 @@ GEM
64
72
  regexp_parser (2.11.3)
65
73
  reline (0.6.2)
66
74
  io-console (~> 0.5)
75
+ rexml (3.4.4)
67
76
  rspec (3.13.2)
68
77
  rspec-core (~> 3.13.0)
69
78
  rspec-expectations (~> 3.13.0)
@@ -110,12 +119,19 @@ GEM
110
119
  unicode-emoji (~> 4.1)
111
120
  unicode-emoji (4.1.0)
112
121
  uri (1.0.4)
122
+ vcr (6.3.1)
123
+ base64
124
+ webmock (3.25.2)
125
+ addressable (>= 2.8.0)
126
+ crack (>= 0.3.2)
127
+ hashdiff (>= 0.4.0, < 2.0.0)
113
128
 
114
129
  PLATFORMS
115
130
  arm64-darwin-23
116
131
  ruby
117
132
 
118
133
  DEPENDENCIES
134
+ cgi (>= 0.5.0)
119
135
  dotenv (>= 3.1.4)
120
136
  irb (>= 1.15.2)
121
137
  rake (>= 13.2.1)
@@ -124,6 +140,8 @@ DEPENDENCIES
124
140
  rubocop-rspec (>= 3.1.0)
125
141
  simplecov (>= 0.22.0)
126
142
  usps-imis-api!
143
+ vcr (>= 6.3.1)
144
+ webmock (>= 3.25.2)
127
145
 
128
146
  BUNDLED WITH
129
147
  2.5.6
data/Readme.md CHANGED
@@ -213,7 +213,7 @@ end
213
213
  ### Field Mapper
214
214
 
215
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:
216
+ Mapper class to further simplify the `fetch` / `update` interfaces:
217
217
 
218
218
  ```ruby
219
219
  mm = api.mapper.fetch(:mm)
@@ -234,6 +234,7 @@ api[:mm]
234
234
 
235
235
  ```ruby
236
236
  api.update(mm: 15)
237
+ api[:mm] = 15
237
238
  ```
238
239
 
239
240
  If there is no known mapping for the requested field, the Mapper will give up, but will provide
@@ -272,6 +273,7 @@ ordinal = created.raw['Identity']['IdentityElements']['$values'][1].to_i # Value
272
273
  vsc.update(certificate: 'E136924', year: 2024, count: 43, ordinal: ordinal)
273
274
 
274
275
  vsc.put_fields(ordinal, 'Quantity' => 44)
276
+ vsc['Quantity'] = 44
275
277
 
276
278
  vsc.destroy(ordinal)
277
279
  ```
@@ -305,7 +307,12 @@ api.with(31092) do
305
307
  mapper.update(mm: 15)
306
308
 
307
309
  update(mm: 15)
310
+
311
+ mapper[:mm] = 15
308
312
  end
313
+
314
+ # This request fetches the same data, but leaves the iMIS ID selected
315
+ api.with(31092)[:mm] = 15
309
316
  ```
310
317
 
311
318
  ```ruby
@@ -333,8 +340,9 @@ api.with(31092) do
333
340
  on('ABC_ASC_Individual_Demog')['TotMMS']
334
341
  end
335
342
 
336
- # This request fetches the same data, but leaves the iMIS ID selected
343
+ # These requests fetch the same data, but leave the iMIS ID selected
337
344
  api.with(31092).on('ABC_ASC_Individual_Demog').get_field('TotMMS')
345
+ api.with(31092).on('ABC_ASC_Individual_Demog')['TotMMS']
338
346
  ```
339
347
 
340
348
  ### Data Methods
@@ -369,6 +377,8 @@ Testing is available by running:
369
377
  bundle exec rspec
370
378
  ```
371
379
 
380
+ API web requests are sanitized and recorded using VCR.
381
+
372
382
  Linting is available by running:
373
383
 
374
384
  ```ruby
data/lib/usps/imis/api.rb CHANGED
@@ -36,11 +36,10 @@ 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)
42
+ authenticate
44
43
  self.imis_id = imis_id if imis_id
45
44
  end
46
45
 
@@ -104,9 +103,7 @@ module Usps
104
103
  #
105
104
  # @return [Hash] Response data from the API
106
105
  #
107
- def query(query_name, query_params = {})
108
- Query.new(self, query_name, query_params)
109
- end
106
+ def query(query_name, query_params = {}) = Query.new(self, query_name, **query_params)
110
107
 
111
108
  # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
112
109
  #
@@ -133,6 +130,11 @@ module Usps
133
130
  def fetch(field_key) = mapper.fetch(field_key)
134
131
  alias [] fetch
135
132
 
133
+ # Convenience alias for updating mapped fields
134
+ #
135
+ def put_field(field_key, value) = update(field_key => value)
136
+ alias []= put_field
137
+
136
138
  # Convenience alias for updating mapped fields
137
139
  #
138
140
  def update(data) = mapper.update(data)
@@ -157,20 +159,22 @@ module Usps
157
159
  def authenticate
158
160
  logger.debug 'Authenticating with iMIS'
159
161
 
160
- uri = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
161
- req = Net::HTTP::Post.new(uri)
162
- authentication_data = {
162
+ request = http_post
163
+ request.body = URI.encode_www_form(
163
164
  grant_type: 'password',
164
165
  username: Imis.configuration.username,
165
166
  password: Imis.configuration.password
166
- }
167
- req.body = URI.encode_www_form(authentication_data)
168
- result = submit(uri, req)
167
+ )
168
+ result = submit(uri, request)
169
169
  json = JSON.parse(result.body)
170
170
 
171
171
  @token = json['access_token']
172
172
  @token_expiration = Time.parse(json['.expires'])
173
173
  end
174
+
175
+ # URI for the authentication endpoint
176
+ #
177
+ def uri(...) = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
174
178
  end
175
179
  end
176
180
  end
@@ -66,6 +66,16 @@ module Usps
66
66
  end
67
67
  alias fetch_all get_fields
68
68
 
69
+ # Update a single named field on a business object for the current member
70
+ #
71
+ # @param field [String] Name of the field
72
+ # @param value Value of the field
73
+ #
74
+ # @return [Usps::Imis::Data] Response data from the API
75
+ #
76
+ def put_field(field, value) = put(filter_fields(field => value))
77
+ alias []= put_field
78
+
69
79
  # Update only specific fields on a business object for the current member
70
80
  #
71
81
  # @param fields [Hash] Conforms to pattern +{ field_key => value }+
@@ -83,7 +93,7 @@ module Usps
83
93
  #
84
94
  # @return [Usps::Imis::Data] Response data from the API
85
95
  #
86
- def put(body) = put_object(Net::HTTP::Put.new(uri), body)
96
+ def put(body) = put_object(http_put, body)
87
97
  alias update put
88
98
 
89
99
  # Create a business object for the current member
@@ -92,14 +102,14 @@ module Usps
92
102
  #
93
103
  # @return [Usps::Imis::Data] Response data from the API
94
104
  #
95
- def post(body) = put_object(Net::HTTP::Post.new(uri(id: '')), body)
105
+ def post(body) = put_object(http_post, body)
96
106
  alias create post
97
107
 
98
108
  # Remove a business object for the current member
99
109
  #
100
110
  # @return [true] Only on success response (i.e. blank string from the API)
101
111
  #
102
- def delete = submit(uri, authorize(Net::HTTP::Delete.new(uri))).body == '' # rubocop:disable Naming/PredicateMethod
112
+ def delete = submit(uri, authorize(http_delete)).body == '' # rubocop:disable Naming/PredicateMethod
103
113
  alias destroy delete
104
114
 
105
115
  # Ruby 3.5 instance variable filter
@@ -163,9 +173,8 @@ module Usps
163
173
  # Useful for stubbing data in tests
164
174
  #
165
175
  def raw_object
166
- request = Net::HTTP::Get.new(uri)
167
- result = submit(uri, authorize(request))
168
- result = Data.from_json(result.body)
176
+ response = submit(uri, authorize(http_get))
177
+ result = Data.from_json(response.body)
169
178
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
170
179
  result
171
180
  end
@@ -174,8 +183,8 @@ module Usps
174
183
  #
175
184
  def put_object(request, body)
176
185
  request.body = JSON.dump(body)
177
- result = submit(uri, authorize(request))
178
- result = Data.from_json(result.body)
186
+ response = submit(uri, authorize(request))
187
+ result = Data.from_json(response.body)
179
188
  JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
180
189
  result
181
190
  end
@@ -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)
@@ -17,6 +17,8 @@ module Usps
17
17
  def initialize(message, metadata = {})
18
18
  super(message)
19
19
  @metadata = metadata
20
+
21
+ Imis.logger(self.class.name).error self
20
22
  end
21
23
 
22
24
  # Additional metadata to include in Bugsnag reports
@@ -28,6 +28,16 @@ module Usps
28
28
  @api.imis_id = imis_id if imis_id
29
29
  end
30
30
 
31
+ # Get a member's data for a specific field by arbitrary field name
32
+ #
33
+ # Does not require knowing which business object / iMIS-specific field name to use
34
+ #
35
+ # Only available for fields defined in +FIELD_MAPPING+
36
+ #
37
+ # @param field_key [Symbol] Internal name of the field
38
+ #
39
+ # @return Value of the field
40
+ #
31
41
  def fetch(field_key)
32
42
  missing_mapping!(field_key) unless FIELD_MAPPING.key?(field_key.to_sym)
33
43
 
@@ -36,6 +46,19 @@ module Usps
36
46
  end
37
47
  alias [] fetch
38
48
 
49
+ # Update a member's data for a specific field by arbitrary field name
50
+ #
51
+ # Does not require knowing which business object / iMIS-specific field name to use
52
+ #
53
+ # Only available for fields defined in +FIELD_MAPPING+
54
+ #
55
+ # @param field_key [Symbol] Internal name of the field
56
+ #
57
+ # @return Value of the field
58
+ #
59
+ def put_field(field_key, value) = update(field_key => value)
60
+ alias []= put_field
61
+
39
62
  # Update a member's data on multiple affected business objects by arbitrary field names
40
63
  #
41
64
  # Does not require knowing which business object / iMIS-specific field name to use
@@ -49,6 +49,16 @@ module Usps
49
49
  def get_fields(ordinal, *fields) = api.on(business_object, ordinal:).get_fields(*fields)
50
50
  alias fetch_all get_fields
51
51
 
52
+ # Update a single named field on a business object for the current member
53
+ #
54
+ # @param field [String] Name of the field
55
+ # @param value Value of the field
56
+ #
57
+ # @return [Usps::Imis::Data] Response data from the API
58
+ #
59
+ def put_field(field, value) = api.on(business_object, ordinal:).put_field(field, value)
60
+ alias []= put_field
61
+
52
62
  # Update only specific fields on a Panel for the current member
53
63
  #
54
64
  # @param ordinal [Integer] The ordinal identifier for the desired object
@@ -24,20 +24,32 @@ module Usps
24
24
  #
25
25
  attr_reader :query_params
26
26
 
27
+ # Current page size for paging through the Query
28
+ #
29
+ attr_accessor :page_size
30
+
27
31
  # Current offset for paging through the Query
28
32
  #
29
- attr_reader :offset
33
+ attr_accessor :offset
34
+
35
+ # Count of records processed
36
+ #
37
+ attr_reader :count
30
38
 
31
39
  # A new instance of +Query+
32
40
  #
33
41
  # @param api [Api] Parent to use for making requests
34
42
  # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
35
- # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
43
+ # @param page_size [Integer] Number of records to return on each request page
44
+ # @param offset [Integer] Offset index of records to return on next request page
45
+ # @param query_params [Hash] Conforms to pattern +{ param_name => param_value }+
36
46
  #
37
- def initialize(api, query_name, query_params)
47
+ def initialize(api, query_name, page_size: 100, offset: nil, **query_params)
38
48
  @api = api
39
49
  @query_name = query_name
40
50
  @query_params = query_params
51
+ @page_size = page_size
52
+ @offset = offset
41
53
  end
42
54
 
43
55
  # Iterate through all results from the query
@@ -53,26 +65,37 @@ module Usps
53
65
  # Iterate through all results from the query, fetching one page at a time
54
66
  #
55
67
  def find_each(&)
56
- result = { 'HasNext' => true }
57
- count = 0
68
+ result = reset!
58
69
 
59
70
  while result['HasNext']
60
- logger.info 'Fetching IQA Query page'
71
+ result = fetch_next.tap do |result_page|
72
+ result_page['Items']['$values'].map { it.except('$type') }.each(&)
73
+ end
74
+ end
61
75
 
62
- result = fetch
76
+ nil
77
+ end
63
78
 
64
- count += result['Count'] || 0
65
- logger.info " -> #{count} / #{result['TotalCount']} #{'item'.pluralize(count)}"
66
- logger.debug ' -> Query page data:'
67
- JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
79
+ # Fetch a raw query page
80
+ #
81
+ def fetch = JSON.parse(submit(uri, authorize(http_get)).body)
68
82
 
69
- items = result['Items']['$values'].map { it.except('$type') }
70
- @offset = result['NextOffset']
83
+ # Fetch the next raw query page, and update the current offset
84
+ #
85
+ def fetch_next
86
+ logger.info 'Fetching IQA Query page'
71
87
 
72
- items.each(&)
73
- end
88
+ result = fetch
74
89
 
75
- nil
90
+ @count += result['Count'] || 0
91
+ total = result['TotalCount']
92
+ logger.info " -> #{@count} / #{total} #{'item'.pluralize(total)}"
93
+ logger.debug ' -> Query page data:'
94
+ JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
95
+
96
+ @offset = result['NextOffset']
97
+
98
+ result
76
99
  end
77
100
 
78
101
  # Ruby 3.5 instance variable filter
@@ -84,11 +107,20 @@ module Usps
84
107
  def token = api.token
85
108
  def token_expiration = api.token_expiration
86
109
 
87
- def path = "#{QUERY_PATH}?#{query_params.merge(QueryName: query_name, Offset: offset).to_query}"
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}"
88
112
  def uri = URI(File.join(Imis.configuration.hostname, path))
89
- def fetch = JSON.parse(submit(uri, authorize(Net::HTTP::Get.new(uri))).body)
90
113
 
91
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
92
124
  end
93
125
  end
94
126
  end
@@ -9,6 +9,11 @@ module Usps
9
9
 
10
10
  def logger = Imis.logger
11
11
 
12
+ def http_get = Net::HTTP::Get.new(uri)
13
+ def http_put = Net::HTTP::Put.new(uri)
14
+ def http_post = Net::HTTP::Post.new(uri(id: ''))
15
+ def http_delete = Net::HTTP::Delete.new(uri)
16
+
12
17
  def client(uri)
13
18
  Net::HTTP.new(uri.host, uri.port).tap do |http|
14
19
  http.use_ssl = true
@@ -45,9 +50,8 @@ module Usps
45
50
  body = request.body.dup
46
51
 
47
52
  Imis.config.filtered_parameters.each do |parameter|
48
- body =
49
- body.gsub(Imis.config.public_send(parameter), '[FILTERED]')
50
- .gsub(CGI.escape(Imis.config.public_send(parameter)), '[FILTERED]')
53
+ body.gsub!(Imis.config.public_send(parameter), '[FILTERED]')
54
+ body.gsub!(CGI.escape(Imis.config.public_send(parameter)), '[FILTERED]')
51
55
  end
52
56
 
53
57
  body
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- VERSION = '0.9.7'
5
+ VERSION = '0.9.9'
6
6
  end
7
7
  end
File without changes
@@ -0,0 +1,67 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://abcdev.imiscloud.com/Token
6
+ body:
7
+ encoding: US-ASCII
8
+ string: grant_type=password&username=<USERNAME>&password=<PASSWORD>
9
+ headers:
10
+ Accept-Encoding:
11
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
12
+ Accept:
13
+ - "*/*"
14
+ User-Agent:
15
+ - Ruby
16
+ Host:
17
+ - abcdev.imiscloud.com
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Date:
24
+ - Sun, 26 Oct 2025 01:24:38 GMT
25
+ Content-Type:
26
+ - application/json; charset=UTF-8
27
+ Transfer-Encoding:
28
+ - chunked
29
+ Connection:
30
+ - keep-alive
31
+ Server:
32
+ - cloudflare
33
+ Cf-Ray:
34
+ - 994627bdc9e8e822-ORD
35
+ Cf-Cache-Status:
36
+ - DYNAMIC
37
+ Access-Control-Allow-Origin:
38
+ - "*"
39
+ Cache-Control:
40
+ - no-cache
41
+ Expires:
42
+ - "-1"
43
+ Strict-Transport-Security:
44
+ - max-age=31536000
45
+ Vary:
46
+ - Accept-Encoding,Accept-Encoding
47
+ Pragma:
48
+ - no-cache
49
+ Access-Control-Expose-Headers:
50
+ - Request-Context
51
+ Request-Context:
52
+ - appId=cid-v1:9c05b5dd-9acf-4a59-aabb-1775785c005f
53
+ X-Aspnet-Version:
54
+ - 4.0.30319
55
+ X-Content-Type-Options:
56
+ - nosniff
57
+ - nosniff
58
+ Set-Cookie:
59
+ - __cf_bm=WthwCIYBGJjeQcPIuJteHBCsffR9_YnIswA1GxBsAY0-1761441878-1.0.1.1-PDKN99A_uKww6R0a3o7xNsGHGxn2nOOhCXcaMzV_6LgrHJ1GEYNmKklmEtAghMWUP4IGpM9INbgu7tFJP1_AlEsAbYw0VOZD80N9_B0VvwU;
60
+ path=/; expires=Sun, 26-Oct-25 01:54:38 GMT; domain=.abcdev.imiscloud.com;
61
+ HttpOnly; Secure; SameSite=None
62
+ body:
63
+ encoding: ASCII-8BIT
64
+ string: '{"access_token":"<ACCESS_TOKEN>","token_type":"bearer","expires_in":3599,"userName":"J.S.FIANDER@GMAIL.COM",".issued":"Sun,
65
+ 26 Oct 2025 01:24:38 GMT",".expires":"Sun, 26 Oct 2025 02:24:38 GMT"}'
66
+ recorded_at: Sun, 26 Oct 2025 01:24:38 GMT
67
+ recorded_with: VCR 6.3.1