usps-imis-api 0.11.25 → 1.0.0.pre.rc.1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +57 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +88 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov +8 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +95 -0
  10. data/Rakefile +12 -0
  11. data/Readme.md +191 -19
  12. data/bin/console +21 -0
  13. data/bin/setup +8 -0
  14. data/lib/ext/hash.rb +10 -0
  15. data/lib/usps/imis/api.rb +138 -177
  16. data/lib/usps/imis/config.rb +10 -68
  17. data/lib/usps/imis/error/api.rb +26 -0
  18. data/lib/usps/imis/error/mapper.rb +9 -0
  19. data/lib/usps/imis/{errors/response_error.rb → error/response.rb} +7 -34
  20. data/lib/usps/imis/mapper.rb +21 -90
  21. data/lib/usps/imis/panel/base_panel.rb +42 -0
  22. data/lib/usps/imis/panel/education.rb +111 -0
  23. data/lib/usps/imis/panel/vsc.rb +109 -0
  24. data/lib/usps/imis/version.rb +1 -1
  25. data/lib/usps/imis.rb +17 -32
  26. data/spec/lib/usps/imis/api_spec.rb +143 -0
  27. data/spec/lib/usps/imis/config_spec.rb +33 -0
  28. data/spec/lib/usps/imis/error/api_spec.rb +17 -0
  29. data/spec/lib/usps/imis/error/response_spec.rb +107 -0
  30. data/spec/lib/usps/imis/mapper_spec.rb +31 -0
  31. data/spec/lib/usps/imis/panel/base_panel_spec.rb +32 -0
  32. data/spec/lib/usps/imis/panel/education_spec.rb +55 -0
  33. data/spec/lib/usps/imis/panel/vsc_spec.rb +38 -0
  34. data/spec/lib/usps/imis_spec.rb +11 -0
  35. data/spec/spec_helper.rb +35 -0
  36. data/usps-imis-api.gemspec +18 -0
  37. metadata +33 -97
  38. data/bin/imis +0 -8
  39. data/lib/usps/imis/base_data.rb +0 -62
  40. data/lib/usps/imis/business_object.rb +0 -220
  41. data/lib/usps/imis/command_line/interface.rb +0 -158
  42. data/lib/usps/imis/command_line/options_parser.rb +0 -136
  43. data/lib/usps/imis/command_line/performers.rb +0 -80
  44. data/lib/usps/imis/command_line.rb +0 -15
  45. data/lib/usps/imis/data.rb +0 -56
  46. data/lib/usps/imis/error.rb +0 -55
  47. data/lib/usps/imis/errors/api_error.rb +0 -11
  48. data/lib/usps/imis/errors/command_line_error.rb +0 -11
  49. data/lib/usps/imis/errors/config_error.rb +0 -11
  50. data/lib/usps/imis/errors/locked_id_error.rb +0 -15
  51. data/lib/usps/imis/errors/mapper_error.rb +0 -29
  52. data/lib/usps/imis/errors/missing_id_error.rb +0 -15
  53. data/lib/usps/imis/errors/not_found_error.rb +0 -11
  54. data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
  55. data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
  56. data/lib/usps/imis/logger.rb +0 -19
  57. data/lib/usps/imis/logger_formatter.rb +0 -32
  58. data/lib/usps/imis/logger_helpers.rb +0 -17
  59. data/lib/usps/imis/mocks/business_object.rb +0 -47
  60. data/lib/usps/imis/mocks.rb +0 -11
  61. data/lib/usps/imis/panels/base_panel.rb +0 -158
  62. data/lib/usps/imis/panels/education.rb +0 -33
  63. data/lib/usps/imis/panels/vsc.rb +0 -32
  64. data/lib/usps/imis/panels.rb +0 -25
  65. data/lib/usps/imis/party_data.rb +0 -88
  66. data/lib/usps/imis/properties.rb +0 -50
  67. data/lib/usps/imis/query.rb +0 -153
  68. data/lib/usps/imis/requests.rb +0 -68
  69. data/spec/support/usps/vcr/config.rb +0 -47
  70. data/spec/support/usps/vcr/filters.rb +0 -89
  71. data/spec/support/usps/vcr.rb +0 -8
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pp'
4
- require 'stringio'
5
-
6
- require_relative 'base_data'
7
-
8
- module Usps
9
- module Imis
10
- # Convenience wrapper for accessing specific properties within an API data response for the
11
- # Party Business Object, which has a different internal structure than others
12
- #
13
- class PartyData < BaseData
14
- # The Business Object name
15
- #
16
- def entity = 'Party'
17
-
18
- # Access the iMIS ID property
19
- #
20
- def imis_id = self['Id'].to_i
21
- alias id imis_id
22
-
23
- # Access the certificate number
24
- #
25
- def certificate = self['AlternateIds'].find { it['IdType'] == 'MajorKey' }['Id']
26
-
27
- # Access an individual property value by name
28
- #
29
- def [](property_name) = properties[property_name]
30
-
31
- # Hash of all property names to values
32
- #
33
- def properties
34
- @properties ||= format_extracted(extract_hash(raw))
35
- end
36
-
37
- private
38
-
39
- def pretty_print_data
40
- { entity:, imis_id: }.compact
41
- end
42
-
43
- def extract_hash(hash)
44
- hash.filter_map do |key, value|
45
- next if key == '$type'
46
-
47
- [key, extract_value(value)]
48
- end
49
- end
50
-
51
- def extract_value(value)
52
- case value
53
- when String, Integer, true, false, nil then value
54
- when Array then value.map { extract_value(it) }
55
- when Hash then extract_hash_value(value)
56
- else
57
- raise Errors::ApiError, "Unrecognized value type: #{value.inspect}"
58
- end
59
- end
60
-
61
- def extract_hash_value(value)
62
- return value['$value'] if value.key?('$value')
63
- return extract_value(value['$values']) if value.key?('$values')
64
- return [value['Name'], extract_value(value['Value'])] if value.key?('Name') && value.key?('Value')
65
-
66
- extract_hash(value)
67
- end
68
-
69
- def format_extracted(data)
70
- hash = data.to_h
71
-
72
- hash.each do |key, value|
73
- case value
74
- when String, Integer, true, false then next
75
- end
76
-
77
- if %w[Addresses AlternateIds CommunicationPreferences Emails Salutations].include?(key)
78
- hash[key] = value.map { format_extracted(it.to_h) }
79
- elsif value.all? { it.is_a?(String) }
80
- # Do nothing
81
- else
82
- hash[key] = format_extracted(value.to_h)
83
- end
84
- end
85
- end
86
- end
87
- end
88
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # Constructor for the Properties field
6
- #
7
- class Properties
8
- # Build the data for a new Properties field
9
- #
10
- def self.build(&) = new.build(&)
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
-
25
- # Build the data for the Properties field
26
- #
27
- def build
28
- yield(self)
29
-
30
- {
31
- 'Properties' => {
32
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyDataCollection, Asi.Contracts',
33
- '$values' => @properties
34
- }
35
- }
36
- end
37
-
38
- # Add an individual property to the field
39
- #
40
- def add(name, value)
41
- @properties ||= []
42
- @properties << {
43
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
44
- 'Name' => name,
45
- 'Value' => self.class.wrap(value)
46
- }
47
- end
48
- end
49
- end
50
- end
@@ -1,153 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # API wrapper for IQA and Business Object Queries
6
- #
7
- class Query
8
- include Enumerable
9
- include Requests
10
-
11
- # Endpoint for IQA query requests
12
- #
13
- IQA_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 page size for paging through the Query
28
- #
29
- attr_accessor :page_size
30
-
31
- # Current offset for paging through the Query
32
- #
33
- attr_accessor :offset
34
-
35
- # Count of records processed
36
- #
37
- attr_reader :count
38
-
39
- # Whether the current query has a next page
40
- #
41
- attr_reader :next_page
42
-
43
- # Tagged logger
44
- #
45
- attr_reader :logger
46
-
47
- # A new instance of +Query+
48
- #
49
- # @param api [Api] Parent to use for making requests
50
- # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
51
- # @param page_size [Integer] Number of records to return on each request page
52
- # @param offset [Integer] Offset index of records to return on next request page
53
- # @param query_params [Hash] Conforms to pattern +{ param_name => param_value }+
54
- #
55
- def initialize(api, query_name, page_size: 100, offset: nil, **query_params)
56
- @api = api
57
- @query_name = query_name
58
- @query_params = query_params
59
- @page_size = page_size
60
- @offset = offset
61
- @count = 0
62
- @logger ||= Imis.logger('Query', query_type)
63
-
64
- logger.tagged('Name').debug query_name
65
- logger.tagged('Params').json query_params
66
- logger.tagged('URI').debug uri
67
- logger.tagged('Page Size').debug page_size
68
- logger.tagged('Offset').debug offset.to_i
69
- end
70
-
71
- # Iterate through all results from the query
72
- #
73
- def each(&)
74
- logger.info 'Running'
75
-
76
- items = []
77
- find_each { items << it }
78
- items.each(&)
79
- end
80
-
81
- # Iterate through all results from the query, fetching one page at a time
82
- #
83
- def find_each(&)
84
- reset!
85
- page.each(&) while page?
86
- nil
87
- end
88
-
89
- # Fetch a filtered query page, and update the current offset
90
- #
91
- def page = fetch_next['Items']['$values'].map { iqa? ? it.except('$type') : Imis::Data[it] }
92
-
93
- # Fetch the next raw query page, and update the current offset
94
- #
95
- def fetch_next
96
- return unless page?
97
-
98
- logger.info "Fetching #{query_type} Query page"
99
-
100
- result = fetch
101
-
102
- @count += result['Count'] || 0
103
- total = result['TotalCount']
104
- logger.info "#{@count} / #{total} #{'item'.pluralize(total)}"
105
- logger.debug 'Query page data:'
106
- logger.json result
107
-
108
- @offset = result['NextOffset']
109
- @next_page = result['HasNext']
110
-
111
- result
112
- end
113
-
114
- # Fetch a raw query page
115
- #
116
- def fetch = JSON.parse(submit(uri, authorize(http_get)).body)
117
-
118
- # Reset query paging progress
119
- #
120
- def reset!
121
- return if next_page.nil?
122
-
123
- logger.debug 'Resetting progress'
124
-
125
- @count = 0
126
- @offset = 0
127
- @next_page = nil
128
- end
129
-
130
- # Ruby 3.5 instance variable filter
131
- #
132
- def instance_variables_to_inspect = instance_variables - %i[@api @logger]
133
-
134
- private
135
-
136
- # Only skip if explicitly set
137
- def page? = next_page.nil? || next_page
138
-
139
- def iqa? = query_name.match?(/^\$/)
140
- def query_type = iqa? ? :IQA : :'Business Object'
141
- def path_params = query_params.merge({ Offset: offset, Limit: page_size }.compact)
142
- def uri = URI(File.join(Imis.configuration.hostname, path))
143
-
144
- def path
145
- if iqa?
146
- "#{IQA_PATH}?#{path_params.merge(QueryName: query_name).to_query}"
147
- else
148
- "#{Imis::BusinessObject::API_PATH}/#{query_name}?#{path_params.to_query}"
149
- end
150
- end
151
- end
152
- end
153
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # @private
6
- #
7
- module Requests
8
- private
9
-
10
- def logger = raise("#{self.class.name} must implement #logger")
11
-
12
- def token = api.token
13
- def token_expiration = api.token_expiration
14
- def authenticate = api.send(:authenticate)
15
-
16
- def http_get = Net::HTTP::Get.new(uri)
17
- def http_put = Net::HTTP::Put.new(uri)
18
- def http_post = Net::HTTP::Post.new(uri(id: ''))
19
- def http_delete = Net::HTTP::Delete.new(uri)
20
-
21
- def client(uri)
22
- Net::HTTP.new(uri.host, uri.port).tap do |http|
23
- http.use_ssl = true
24
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
25
- end
26
- end
27
-
28
- # Authorize a request prior to submitting
29
- #
30
- # If the current token is missing/expired, request a new one
31
- #
32
- def authorize(request)
33
- if token_expiration.nil? || token_expiration < Time.now
34
- logger.debug 'Token expired: re-authenticating with iMIS'
35
- authenticate
36
- end
37
- request.tap { it.add_field('Authorization', "Bearer #{token}") }
38
- end
39
-
40
- def submit(uri, request)
41
- logger.info 'Submitting request to iMIS'
42
- logger.tagged('Request') do
43
- logger.debug "#{request.class.name.demodulize.upcase} #{uri}"
44
- logger.multiline sanitized_request_body(request)
45
-
46
- client(uri).request(request).tap do |result|
47
- raise Errors::ResponseError.from(result) unless result.is_a?(Net::HTTPSuccess)
48
-
49
- logger.info 'Request succeeded'
50
- end
51
- end
52
- end
53
-
54
- def sanitized_request_body(request)
55
- return '*** empty request body ***' if request.body.nil?
56
-
57
- body = request.body.dup
58
-
59
- Imis.config.filtered_parameters.each do |parameter|
60
- body.gsub!(Imis.config.public_send(parameter), '[FILTERED]')
61
- body.gsub!(CGI.escape(Imis.config.public_send(parameter)), '[FILTERED]')
62
- end
63
-
64
- body
65
- end
66
- end
67
- end
68
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Vcr
5
- module Config
6
- class << self
7
- def configure!
8
- WebMock.disable_net_connect!
9
-
10
- VCR.configure do |config|
11
- config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
12
- config.hook_into :webmock
13
- default_options(config)
14
- config.configure_rspec_metadata!
15
- apply_filters(config)
16
-
17
- yield(config) if block_given?
18
- end
19
- end
20
-
21
- def vcr_record_ordered
22
- ENV['VCR'] == 'all' ? :defined : :random
23
- end
24
-
25
- private
26
-
27
- def default_options(config)
28
- config.default_cassette_options = {
29
- record: ENV['VCR'] ? ENV['VCR'].to_sym : :once,
30
- match_requests_on: %i[method uri]
31
- }
32
- end
33
-
34
- def apply_filters(config)
35
- Filters.apply!(
36
- config,
37
- *%i[
38
- username password access_token bearer_token
39
- ignore_response_headers cf_ray cookie date
40
- issued expires
41
- ]
42
- )
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Vcr
5
- module Filters
6
- class << self
7
- def apply!(config, *filters)
8
- filters.each { public_send(it, config) }
9
- end
10
-
11
- def username(config)
12
- value = Usps::Imis.config.username || '<USERNAME>'
13
- config.filter_sensitive_data('<USERNAME>') { value }
14
- config.filter_sensitive_data('<USERNAME>') { CGI.escape(value) }
15
- config.filter_sensitive_data('<USERNAME>') { value.upcase }
16
- config.filter_sensitive_data('<USERNAME>') { CGI.escape(value).upcase }
17
- end
18
-
19
- def password(config)
20
- value = Usps::Imis.config.password || '<PASSWORD>'
21
- config.filter_sensitive_data('<PASSWORD>') { value }
22
- config.filter_sensitive_data('<PASSWORD>') { CGI.escape(value) }
23
- end
24
-
25
- def access_token(config)
26
- filter_json_field(config, 'access_token', '<ACCESS_TOKEN>')
27
- end
28
-
29
- def bearer_token(config)
30
- config.filter_sensitive_data('<BEARER_TOKEN>') do |interaction|
31
- if interaction.request.headers['Authorization']
32
- authorization = interaction.request.headers['Authorization'].first
33
- if (match = authorization.match(/^Bearer\s+([^,\s]+)/))
34
- match.captures.first
35
- end
36
- end
37
- end
38
- end
39
-
40
- def ignore_response_headers(config)
41
- config.before_record do |interaction|
42
- interaction.response.headers.delete('Report-To')
43
- interaction.response.headers.delete('Content-Security-Policy-Report-Only')
44
- end
45
- end
46
-
47
- def cf_ray(config)
48
- config.filter_sensitive_data('<CF_RAY>') do |interaction|
49
- interaction.response.headers['Cf-Ray']&.first
50
- end
51
- end
52
-
53
- def cookie(config)
54
- config.filter_sensitive_data('<COOKIE>') do |interaction|
55
- interaction.response.headers['Set-Cookie']&.first
56
- end
57
- end
58
-
59
- def date(config)
60
- config.filter_sensitive_data('<DATE>') do |interaction|
61
- interaction.response.headers['Date']&.first
62
- end
63
- end
64
-
65
- def issued(config)
66
- filter_json_field(config, '.issued', '<ISSUED>')
67
- end
68
-
69
- def expires(config)
70
- filter_json_field(config, '.expires', '<EXPIRES>')
71
- end
72
-
73
- private
74
-
75
- def filter_json_field(config, field_name, placeholder)
76
- config.filter_sensitive_data(placeholder) do |interaction|
77
- if interaction.response.headers['Content-Type']&.first&.include?('application/json')
78
- begin
79
- JSON.parse(interaction.response.body)[field_name]
80
- rescue JSON::ParserError
81
- nil
82
- end
83
- end
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'vcr'
4
- require 'webmock/rspec'
5
- require 'usps/imis'
6
-
7
- require_relative 'vcr/filters'
8
- require_relative 'vcr/config'