usps-imis-api 0.8.1 → 0.9.0

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: ea2c1c51fb9d73e9aa4dc60f4be57c72443f9d2f4e16fcd8e572af8e1b7c58e6
4
- data.tar.gz: ffd74ed752fe1cfc9f0c0eaeb166e1d13bf75cf40d0b07bafaa9ce1de5b09c49
3
+ metadata.gz: e94dcde6abaf560148b72523c9defe96cd30f6980631f11b79cf8b35f52a1dcc
4
+ data.tar.gz: a94d8ce631656ff48bf6c3b905e3e06bf130a5a658dfad737afec2be21ac1957
5
5
  SHA512:
6
- metadata.gz: e3fad87c5d7fa1c3bfa8c0140304b39e7df21d5e9abdc5234a183a1175409c5f42fe8e65f8e53975b127f15004a65aa988d533ce68ed56d3867d470f28353b2b
7
- data.tar.gz: 872e0e9a73136a6166a5c585c8286d106e3d81758e2eccdbc8f8a1b1fc2d845883680e54323ffbd5902f599cbcc685be611ff1edf62c917d5e42b5ec92ba79ef
6
+ metadata.gz: 1c155ff3c3e0801c4cb1d738c736e7da9833b651ba56cafb5983439d8e7708f15a266f688d40672917270745b198cac6a38e86b735932c447a631b1c5922b17d
7
+ data.tar.gz: e99827b9bb433d673c6d76558c209f23a12aba1dbe22a674d4972f8ff97b0122374cb4ef2955c0b903ebcf5f7d20721c15b800b79cba17c304113436ea8c66a9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- usps-imis-api (0.8.1)
4
+ usps-imis-api (0.9.0)
5
5
  activesupport (~> 8.0)
6
6
 
7
7
  GEM
data/Readme.md CHANGED
@@ -32,6 +32,10 @@ Usps::Imis.configure do |config|
32
32
  config.imis_id_query_name = ENV['IMIS_ID_QUERY_NAME']
33
33
  config.username = ENV['IMIS_USERNAME']
34
34
  config.password = ENV['IMIS_PASSWORD']
35
+
36
+ # These options will use these defaults
37
+ config.logger = Logger.new($stdout)
38
+ config.logger.level = :info
35
39
  end
36
40
  ```
37
41
 
@@ -195,7 +199,15 @@ Run an IQA Query
195
199
  `query_params` is a hash of shape: `{ param_name => param_value }`
196
200
 
197
201
  ```ruby
198
- api.query(query_name, query_params)
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
199
211
  ```
200
212
 
201
213
  ### Field Mapper
@@ -314,6 +326,14 @@ end
314
326
  api.with(31092).on('ABC_ASC_Individual_Demog').get_field('TotMMS')
315
327
  ```
316
328
 
329
+ ### Data Methods
330
+
331
+ Data responses from the API can be handled as a standard Hash using the `raw` method.
332
+
333
+ If you need to access all of the property values, you can use the `properties` method.
334
+ By default, this will exclude the `ID` and `Ordinal` properties; they can be included with
335
+ `properties(include_ids: true)`.
336
+
317
337
  ## Test Data Mocking
318
338
 
319
339
  You can use the provided Business Object Mock to generate stub data for rspec:
data/lib/usps/imis/api.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'requests'
4
4
  require_relative 'business_object'
5
5
  require_relative 'mapper'
6
+ require_relative 'query'
6
7
 
7
8
  module Usps
8
9
  module Imis
@@ -15,10 +16,6 @@ module Usps
15
16
  #
16
17
  AUTHENTICATION_PATH = 'Token'
17
18
 
18
- # Endpoint for IQA query requests
19
- #
20
- QUERY_PATH = 'api/Query'
21
-
22
19
  # API bearer token
23
20
  #
24
21
  attr_reader :token
@@ -67,8 +64,7 @@ module Usps
67
64
  raise Errors::LockedIdError if lock_imis_id
68
65
 
69
66
  begin
70
- result = query(Imis.configuration.imis_id_query_name, { certificate: })
71
- @imis_id = result['Items']['$values'][0]['ID'].to_i
67
+ self.imis_id = query(Imis.configuration.imis_id_query_name, { certificate: }).first['ID'].to_i
72
68
  rescue StandardError
73
69
  raise Errors::NotFoundError, 'Member not found'
74
70
  end
@@ -101,7 +97,7 @@ module Usps
101
97
  end
102
98
  end
103
99
 
104
- # Run an IQA Query
100
+ # Build an IQA Query interface
105
101
  #
106
102
  # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
107
103
  # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
@@ -109,31 +105,7 @@ module Usps
109
105
  # @return [Hash] Response data from the API
110
106
  #
111
107
  def query(query_name, query_params = {})
112
- query_params[:QueryName] = query_name
113
- path = "#{QUERY_PATH}?#{query_params.to_query}"
114
- uri = URI(File.join(Imis.configuration.hostname, path))
115
- request = Net::HTTP::Get.new(uri)
116
- result = submit(uri, authorize(request))
117
- JSON.parse(result.body)
118
- end
119
-
120
- # Run an IQA Query, paging through all responses
121
- #
122
- # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
123
- # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
124
- #
125
- # @return [Array<Hash>] Collected response item values from the API
126
- #
127
- def query_all(query_name, query_params = {})
128
- response = query(query_name, **query_params)
129
- results = response['Items']['$values']
130
-
131
- while response['HasNext']
132
- response = query(query_name, **query_params, Offset: response['NextOffset'])
133
- results += response['Items']['$values']
134
- end
135
-
136
- results
108
+ Query.new(self, query_name, query_params)
137
109
  end
138
110
 
139
111
  # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
@@ -178,6 +150,8 @@ module Usps
178
150
  # Authenticate to the iMIS API, and store the access token and expiration time
179
151
  #
180
152
  def authenticate
153
+ Imis.logger.debug 'Authenticating with iMIS'
154
+
181
155
  uri = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
182
156
  req = Net::HTTP::Post.new(uri)
183
157
  authentication_data = {
@@ -8,16 +8,19 @@ module Usps
8
8
  IMIS_ROOT_URL_PROD = 'https://portal.americasboatingclub.org'
9
9
  IMIS_ROOT_URL_DEV = 'https://abcdev.imiscloud.com'
10
10
 
11
- attr_accessor :imis_id_query_name, :username, :password
12
- attr_reader :environment
11
+ attr_accessor :imis_id_query_name, :username, :password, :logger
12
+ attr_reader :environment, :logger_level
13
13
 
14
14
  def initialize
15
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)
19
+ @logger = Logger.new($stdout, level: :info)
19
20
 
20
21
  yield self if block_given?
22
+
23
+ @logger_level = logger.class::SEV_LABEL[logger.level].downcase.to_sym
21
24
  end
22
25
 
23
26
  def environment=(env)
@@ -37,7 +40,7 @@ module Usps
37
40
 
38
41
  # Ruby 3.5 instance variable filter
39
42
  #
40
- def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username]
43
+ def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username @logger_level]
41
44
  end
42
45
  end
43
46
  end
@@ -16,6 +16,10 @@ module Usps
16
16
 
17
17
  alias raw to_h
18
18
 
19
+ # The Business Object or Panel name
20
+ #
21
+ def entity = raw['EntityTypeName']
22
+
19
23
  # Access the iMIS ID property
20
24
  #
21
25
  def imis_id = self['ID'].to_i
@@ -35,6 +39,17 @@ module Usps
35
39
  value.is_a?(String) ? value : value['$value']
36
40
  end
37
41
 
42
+ # Hash of all property names to values
43
+ #
44
+ # @param include_ids [Boolean] Whether to include the iMIS ID and Ordinal
45
+ #
46
+ def properties(include_ids: false)
47
+ raw['Properties']['$values']
48
+ .map { it['Name'] }
49
+ .select { include_ids || !%w[ID Ordinal].include?(it) }
50
+ .index_with { self[it] }
51
+ end
52
+
38
53
  def inspect
39
54
  stringio = StringIO.new
40
55
  PP.pp(self, stringio)
@@ -42,11 +57,7 @@ module Usps
42
57
  end
43
58
 
44
59
  def pretty_print(pp)
45
- data = {
46
- entity_type_name: raw['EntityTypeName'],
47
- imis_id:,
48
- ordinal:
49
- }.compact
60
+ data = { entity:, imis_id:, ordinal: }.compact
50
61
 
51
62
  pp.group(1, "#<#{self.class}", '>') do
52
63
  data.each do |key, value|
@@ -36,7 +36,7 @@ module Usps
36
36
  #
37
37
  # @param data [Hash] Conforms to pattern +{ field_key => value }+
38
38
  #
39
- # @return [Array<Hash>] Response data from the API for each internal update request
39
+ # @return [Array<Usps::Imis::Data>] Response data from the API for each internal update request
40
40
  #
41
41
  def update(data)
42
42
  updates = data.each_with_object({}) do |(field_key, value), hash|
@@ -54,15 +54,13 @@ module Usps
54
54
  private
55
55
 
56
56
  def map_update(field_name)
57
- if FIELD_MAPPING.key?(field_name.to_sym)
58
- business_object_name, field = FIELD_MAPPING[field_name.to_sym]
59
- yield(business_object_name, field)
60
- else
61
- missing_mapping(field_name)
62
- end
57
+ missing_mapping!(field_name) unless FIELD_MAPPING.key?(field_name.to_sym)
58
+
59
+ business_object_name, field = FIELD_MAPPING[field_name.to_sym]
60
+ yield(business_object_name, field)
63
61
  end
64
62
 
65
- def missing_mapping(field_name)
63
+ def missing_mapping!(field_name)
66
64
  unless ENV['TESTING']
67
65
  # :nocov:
68
66
  warn(
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ # API wrapper for IQA Queries
6
+ #
7
+ class Query
8
+ include Enumerable
9
+ include Requests
10
+
11
+ # Endpoint for IQA query requests
12
+ #
13
+ QUERY_PATH = 'api/Query'
14
+
15
+ # The parent +Api+ object
16
+ #
17
+ attr_reader :api
18
+
19
+ # Name of the Query to run
20
+ #
21
+ attr_reader :query_name
22
+
23
+ # Parameters for the Query
24
+ #
25
+ attr_reader :query_params
26
+
27
+ # Current offset for paging through the Query
28
+ #
29
+ attr_reader :offset
30
+
31
+ # A new instance of +Query+
32
+ #
33
+ # @param api [Api] Parent to use for making requests
34
+ # @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 }+
36
+ #
37
+ def initialize(api, query_name, query_params)
38
+ @api = api
39
+ @query_name = query_name
40
+ @query_params = query_params
41
+ end
42
+
43
+ # Iterate through all results from the query
44
+ #
45
+ def each(&)
46
+ Imis.logger.info 'Running IQA Query on iMIS'
47
+
48
+ items = []
49
+ find_each { items << it }
50
+ items.each(&)
51
+ end
52
+
53
+ # Iterate through all results from the query, fetching one page at a time
54
+ #
55
+ def find_each(&)
56
+ result = { 'HasNext' => true }
57
+ count = 0
58
+
59
+ while result['HasNext']
60
+ Imis.logger.info 'Fetching IQA Query page'
61
+
62
+ result = fetch
63
+
64
+ count += result['Count'] || 0
65
+ Imis.logger.info " -> #{count} / #{result['TotalCount']} #{'item'.pluralize(count)}"
66
+ Imis.logger.debug ' -> Query page data:'
67
+ JSON.pretty_generate(result).split("\n").each { Imis.logger.debug " -> #{it}" }
68
+
69
+ items = result['Items']['$values'].map { it.except('$type') }
70
+ @offset = result['NextOffset']
71
+
72
+ items.each(&)
73
+ end
74
+
75
+ nil
76
+ end
77
+
78
+ private
79
+
80
+ def token = api.token
81
+ def token_expiration = api.token_expiration
82
+
83
+ def path = "#{QUERY_PATH}?#{query_params.merge(QueryName: query_name, Offset: offset).to_query}"
84
+ def uri = URI(File.join(Imis.configuration.hostname, path))
85
+ def fetch = JSON.parse(submit(uri, authorize(Net::HTTP::Get.new(uri))).body)
86
+ end
87
+ end
88
+ end
@@ -19,15 +19,29 @@ module Usps
19
19
  # If the current token is missing/expired, request a new one
20
20
  #
21
21
  def authorize(request)
22
- authenticate if token_expiration < Time.now
22
+ if token_expiration < Time.now
23
+ Imis.logger.debug 'Token expired: re-authenticating with iMIS'
24
+ authenticate
25
+ end
23
26
  request.tap { it.add_field('Authorization', "Bearer #{token}") }
24
27
  end
25
28
 
26
29
  def submit(uri, request)
30
+ Imis.logger.info 'Submitting request to iMIS'
31
+ Imis.logger.debug " -> #{uri}"
32
+ sanitized_request_body(request).split("\n").each { Imis.logger.debug " -> #{it}" }
27
33
  client(uri).request(request).tap do |result|
28
34
  raise Errors::ResponseError.from(result) unless result.is_a?(Net::HTTPSuccess)
35
+
36
+ Imis.logger.info 'Request succeeded'
29
37
  end
30
38
  end
39
+
40
+ def sanitized_request_body(request)
41
+ return 'grant_type=password&username=[filtered]&password=[filtered]' if request.body&.include?('password=')
42
+
43
+ request.body.nil? ? '*** empty request body ***' : JSON.pretty_generate(JSON.parse(request.body))
44
+ end
31
45
  end
32
46
  end
33
47
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- VERSION = '0.8.1'
5
+ VERSION = '0.9.0'
6
6
  end
7
7
  end
data/lib/usps/imis.rb CHANGED
@@ -8,7 +8,9 @@ require 'cgi'
8
8
 
9
9
  require 'active_support/core_ext/string/inflections'
10
10
  require 'active_support/core_ext/object/to_query'
11
+ require 'active_support/core_ext/enumerable'
11
12
  require 'active_support/string_inquirer'
13
+ require 'logger'
12
14
 
13
15
  # Internal requires
14
16
  require_relative 'imis/config'
@@ -39,6 +41,10 @@ module Usps
39
41
  yield(configuration) if block_given?
40
42
  configuration
41
43
  end
44
+
45
+ # Logger instance to write to
46
+ #
47
+ def logger = configuration.logger
42
48
  end
43
49
  end
44
50
  end
@@ -37,18 +37,18 @@ describe Usps::Imis::Api do
37
37
  end
38
38
  end
39
39
 
40
- describe '#query_all' do
40
+ describe '#query' do
41
+ let(:query) { api.query('$/ABC/ExampleQueryAll', {}) }
42
+
41
43
  before do
42
- allow(api).to receive(:query).and_return(
44
+ allow(query).to receive(:fetch).and_return(
43
45
  { 'Items' => { '$values' => [{ 'key1' => 'value1' }] }, 'HasNext' => true, 'NextOffset' => 1 },
44
46
  { 'Items' => { '$values' => [{ 'key1' => 'value2' }] }, 'HasNext' => false, 'NextOffset' => 0 }
45
47
  )
46
48
  end
47
49
 
48
50
  it 'collects all query results' do
49
- expect(api.query_all('$/ABC/ExampleQueryAll')).to eq(
50
- [{ 'key1' => 'value1' }, { 'key1' => 'value2' }]
51
- )
51
+ expect(query.to_a).to eq([{ 'key1' => 'value1' }, { 'key1' => 'value2' }])
52
52
  end
53
53
  end
54
54
 
@@ -56,9 +56,7 @@ describe Usps::Imis::Api do
56
56
  before { api.imis_id = 31092 }
57
57
 
58
58
  it 'sends an update' do
59
- expect(api.on('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15)).to(
60
- be_a(Hash)
61
- )
59
+ expect(api.on('ABC_ASC_Individual_Demog').put_fields('TotMMS' => 15)).to be_a(Hash)
62
60
  end
63
61
 
64
62
  context 'when receiving a response error' do
@@ -22,9 +22,23 @@ describe Usps::Imis::Data do
22
22
  end
23
23
  end
24
24
 
25
+ describe '#properties' do
26
+ it 'iterates over the properties, excluding IDs' do
27
+ expect(data.properties.map { |field, value| "#{field}: #{value}" }).to eq(
28
+ ['Stub Integer: 42', 'Stub String: something']
29
+ )
30
+ end
31
+
32
+ it 'iterates over the properties, including IDs' do
33
+ expect(data.properties(include_ids: true).map { |field, value| "#{field}: #{value}" }).to eq(
34
+ ['ID: 31092', 'Stub Integer: 42', 'Stub String: something']
35
+ )
36
+ end
37
+ end
38
+
25
39
  describe '#inspect' do
26
40
  it 'generates the correct inspect string' do
27
- expect(data.inspect).to eq('#<Usps::Imis::Data entity_type_name="ABC_ASC_Individual_Demog" imis_id=31092>')
41
+ expect(data.inspect).to eq('#<Usps::Imis::Data entity="ABC_ASC_Individual_Demog" imis_id=31092>')
28
42
  end
29
43
 
30
44
  context 'with data from a Panel' do
@@ -44,7 +58,7 @@ describe Usps::Imis::Data do
44
58
 
45
59
  it 'generates the correct inspect string with an ordinal' do
46
60
  expect(data.inspect).to eq(
47
- '#<Usps::Imis::Data entity_type_name="ABC_ASC_Individual_Demog" imis_id=31092 ordinal=99>'
61
+ '#<Usps::Imis::Data entity="ABC_ASC_Individual_Demog" imis_id=31092 ordinal=99>'
48
62
  )
49
63
  end
50
64
  end
data/spec/spec_helper.rb CHANGED
@@ -31,6 +31,8 @@ RSpec.configure do |config|
31
31
 
32
32
  imis_config.username = ENV.fetch('IMIS_USERNAME', '')
33
33
  imis_config.password = ENV.fetch('IMIS_PASSWORD', '')
34
+
35
+ imis_config.logger = Logger.new(nil)
34
36
  end
35
37
  end
36
38
  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: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Fiander
@@ -63,6 +63,7 @@ files:
63
63
  - lib/usps/imis/panels/education.rb
64
64
  - lib/usps/imis/panels/vsc.rb
65
65
  - lib/usps/imis/properties.rb
66
+ - lib/usps/imis/query.rb
66
67
  - lib/usps/imis/requests.rb
67
68
  - lib/usps/imis/version.rb
68
69
  - spec/lib/usps/imis/api_spec.rb