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
@@ -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
  #
@@ -24,26 +24,46 @@ 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
38
+
39
+ # Whether the current query has a next page
40
+ #
41
+ attr_reader :next_page
30
42
 
31
43
  # A new instance of +Query+
32
44
  #
33
45
  # @param api [Api] Parent to use for making requests
34
46
  # @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 }+
47
+ # @param page_size [Integer] Number of records to return on each request page
48
+ # @param offset [Integer] Offset index of records to return on next request page
49
+ # @param query_params [Hash] Conforms to pattern +{ param_name => param_value }+
36
50
  #
37
- def initialize(api, query_name, query_params)
51
+ def initialize(api, query_name, page_size: 100, offset: nil, **query_params)
38
52
  @api = api
39
53
  @query_name = query_name
40
54
  @query_params = query_params
55
+ @query_params.merge!(QueryName: query_name) if iqa?
56
+ @page_size = page_size
57
+ @offset = offset
58
+ @count = 0
59
+
60
+ logger.debug "URI: #{uri}"
41
61
  end
42
62
 
43
63
  # Iterate through all results from the query
44
64
  #
45
65
  def each(&)
46
- logger.info 'Running IQA Query on iMIS'
66
+ logger.info 'Running'
47
67
 
48
68
  items = []
49
69
  find_each { items << it }
@@ -53,26 +73,56 @@ module Usps
53
73
  # Iterate through all results from the query, fetching one page at a time
54
74
  #
55
75
  def find_each(&)
56
- result = { 'HasNext' => true }
57
- count = 0
76
+ reset!
77
+
78
+ while page?
79
+ fetch_next.tap do |result_page|
80
+ result_page['Items']['$values'].map { it.except('$type') }.each(&)
81
+ end
82
+ end
83
+
84
+ nil
85
+ end
58
86
 
59
- while result['HasNext']
60
- logger.info 'Fetching IQA Query page'
87
+ # Fetch a filtered query page, and update the current offset
88
+ #
89
+ def page = fetch_next['Items']['$values'].map { iqa? ? it.except('$type') : Imis::Data[it] }
61
90
 
62
- result = fetch
91
+ # Fetch the next raw query page, and update the current offset
92
+ #
93
+ def fetch_next
94
+ return unless page?
63
95
 
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}" }
96
+ logger.info "Fetching #{query_type} Query page"
68
97
 
69
- items = result['Items']['$values'].map { it.except('$type') }
70
- @offset = result['NextOffset']
98
+ result = fetch
71
99
 
72
- items.each(&)
73
- end
100
+ @count += result['Count'] || 0
101
+ total = result['TotalCount']
102
+ logger.info " -> #{@count} / #{total} #{'item'.pluralize(total)}"
103
+ logger.debug ' -> Query page data:'
104
+ JSON.pretty_generate(result).split("\n").each { logger.debug " -> #{it}" }
74
105
 
75
- nil
106
+ @offset = result['NextOffset']
107
+ @next_page = result['HasNext']
108
+
109
+ result
110
+ end
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
76
126
  end
77
127
 
78
128
  # Ruby 3.5 instance variable filter
@@ -81,14 +131,17 @@ module Usps
81
131
 
82
132
  private
83
133
 
84
- def token = api.token
85
- def token_expiration = api.token_expiration
134
+ # Only skip if explicitly set
135
+ def page? = next_page.nil? || next_page
86
136
 
87
- def path = "#{QUERY_PATH}?#{query_params.merge(QueryName: query_name, Offset: offset).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}"
88
142
  def uri = URI(File.join(Imis.configuration.hostname, path))
89
- def fetch = JSON.parse(submit(uri, authorize(Net::HTTP::Get.new(uri))).body)
90
143
 
91
- def logger = Imis.logger('Query')
144
+ def logger = Imis.logger("#{query_type} Query")
92
145
  end
93
146
  end
94
147
  end
@@ -9,6 +9,15 @@ module Usps
9
9
 
10
10
  def logger = Imis.logger
11
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
+
12
21
  def client(uri)
13
22
  Net::HTTP.new(uri.host, uri.port).tap do |http|
14
23
  http.use_ssl = true
@@ -21,7 +30,7 @@ module Usps
21
30
  # If the current token is missing/expired, request a new one
22
31
  #
23
32
  def authorize(request)
24
- if token_expiration < Time.now
33
+ if token_expiration.nil? || token_expiration < Time.now
25
34
  logger.debug 'Token expired: re-authenticating with iMIS'
26
35
  authenticate
27
36
  end
@@ -45,9 +54,8 @@ module Usps
45
54
  body = request.body.dup
46
55
 
47
56
  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]')
57
+ body.gsub!(Imis.config.public_send(parameter), '[FILTERED]')
58
+ body.gsub!(CGI.escape(Imis.config.public_send(parameter)), '[FILTERED]')
51
59
  end
52
60
 
53
61
  body
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- VERSION = '1.0.0-rc.8'
5
+ VERSION = '1.0.0-rc.10'
6
6
  end
7
7
  end
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,89 @@
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
@@ -0,0 +1,8 @@
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'
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: 1.0.0.pre.rc.8
4
+ version: 1.0.0.pre.rc.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Fiander
@@ -29,18 +29,7 @@ executables: []
29
29
  extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
- - ".github/workflows/main.yml"
33
- - ".gitignore"
34
- - ".rspec"
35
- - ".rubocop.yml"
36
- - ".ruby-version"
37
- - ".simplecov"
38
- - Gemfile
39
- - Gemfile.lock
40
- - Rakefile
41
32
  - Readme.md
42
- - bin/console
43
- - bin/setup
44
33
  - lib/usps/imis.rb
45
34
  - lib/usps/imis/api.rb
46
35
  - lib/usps/imis/business_object.rb
@@ -51,6 +40,7 @@ files:
51
40
  - lib/usps/imis/errors/config_error.rb
52
41
  - lib/usps/imis/errors/locked_id_error.rb
53
42
  - lib/usps/imis/errors/mapper_error.rb
43
+ - lib/usps/imis/errors/missing_id_error.rb
54
44
  - lib/usps/imis/errors/not_found_error.rb
55
45
  - lib/usps/imis/errors/panel_unimplemented_error.rb
56
46
  - lib/usps/imis/errors/response_error.rb
@@ -66,21 +56,9 @@ files:
66
56
  - lib/usps/imis/query.rb
67
57
  - lib/usps/imis/requests.rb
68
58
  - lib/usps/imis/version.rb
69
- - spec/lib/usps/imis/api_spec.rb
70
- - spec/lib/usps/imis/business_object_spec.rb
71
- - spec/lib/usps/imis/config_spec.rb
72
- - spec/lib/usps/imis/data_spec.rb
73
- - spec/lib/usps/imis/error_spec.rb
74
- - spec/lib/usps/imis/errors/response_error_spec.rb
75
- - spec/lib/usps/imis/mapper_spec.rb
76
- - spec/lib/usps/imis/mocks/business_object_spec.rb
77
- - spec/lib/usps/imis/panels/base_panel_spec.rb
78
- - spec/lib/usps/imis/panels/education_spec.rb
79
- - spec/lib/usps/imis/panels/vsc_spec.rb
80
- - spec/lib/usps/imis/properties_spec.rb
81
- - spec/lib/usps/imis_spec.rb
82
- - spec/spec_helper.rb
83
- - usps-imis-api.gemspec
59
+ - spec/support/usps/vcr.rb
60
+ - spec/support/usps/vcr/config.rb
61
+ - spec/support/usps/vcr/filters.rb
84
62
  homepage: https://github.com/unitedstatespowersquadrons/imis-api-ruby
85
63
  licenses: []
86
64
  metadata:
@@ -88,6 +66,7 @@ metadata:
88
66
  rdoc_options: []
89
67
  require_paths:
90
68
  - lib
69
+ - spec/support
91
70
  required_ruby_version: !ruby/object:Gem::Requirement
92
71
  requirements:
93
72
  - - ">="
@@ -1,57 +0,0 @@
1
- name: 'USPS iMIS API - Ruby'
2
- on:
3
- push:
4
- paths:
5
- - '**.rb'
6
- - '**.yml'
7
- - '**.json'
8
- - 'Gemfile'
9
- - '.simplecov'
10
- branches:
11
- - 'main'
12
- - '**'
13
- workflow_dispatch:
14
-
15
- jobs:
16
- rubocop:
17
- runs-on: ubuntu-24.04
18
- environment: "USPS iMIS API - Ruby"
19
- strategy:
20
- matrix:
21
- ruby: ['3.4', head]
22
- steps:
23
- - name: Checkout Code
24
- uses: actions/checkout@v4
25
- - name: Setup Ruby
26
- uses: ruby/setup-ruby@v1
27
- with:
28
- ruby-version: ${{ matrix.ruby }}
29
- bundler-cache: true
30
- - name: Run Rubocop
31
- run: bundle exec rubocop
32
- rspec:
33
- runs-on: ubuntu-24.04
34
- environment: "USPS iMIS API - Ruby"
35
- strategy:
36
- matrix:
37
- ruby: ['3.4', head]
38
- env:
39
- IMIS_USERNAME: ${{ secrets.IMIS_USERNAME }}
40
- IMIS_PASSWORD: ${{ secrets.IMIS_PASSWORD }}
41
- IMIS_ID_QUERY_NAME: ${{ secrets.IMIS_ID_QUERY_NAME }}
42
- steps:
43
- - name: Checkout Code
44
- uses: actions/checkout@v4
45
- - name: Setup Ruby
46
- uses: ruby/setup-ruby@v1
47
- with:
48
- ruby-version: ${{ matrix.ruby }}
49
- bundler-cache: true
50
- - name: Run Rspec
51
- run: bundle exec rspec --format documentation --order rand --color --tty
52
- - name: Store Coverage
53
- uses: actions/upload-artifact@v4
54
- with:
55
- name: coverage-rspec-${{ matrix.ruby }}
56
- include-hidden-files: true
57
- path: coverage/
data/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- coverage/
2
- tmp/
3
- .env
4
- *.gem
5
- .yardoc
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --require spec_helper
2
- --order rand
data/.rubocop.yml DELETED
@@ -1,89 +0,0 @@
1
- plugins:
2
- - rubocop-rspec
3
-
4
- AllCops:
5
- TargetRubyVersion: 3.4
6
- Exclude:
7
- - bin/**/*
8
- - config/**/*
9
- - db/**/*
10
- - vendor/**/*
11
- - tmp/**/*
12
- NewCops: enable
13
- SuggestExtensions: false
14
-
15
- Layout/FirstHashElementIndentation:
16
- EnforcedStyle: consistent
17
- Layout/AccessModifierIndentation:
18
- EnforcedStyle: outdent
19
- Layout/EmptyLinesAroundAccessModifier:
20
- Enabled: true
21
- Layout/ArrayAlignment:
22
- Enabled: true
23
- Layout/HashAlignment:
24
- Enabled: true
25
- Layout/EmptyLineAfterGuardClause:
26
- Enabled: true
27
- Layout/SpaceInsideBlockBraces:
28
- EnforcedStyle: space
29
- EnforcedStyleForEmptyBraces: no_space
30
- Layout/SpaceInsideHashLiteralBraces:
31
- EnforcedStyle: space
32
- EnforcedStyleForEmptyBraces: no_space
33
- Layout/SpaceInsideArrayLiteralBrackets:
34
- EnforcedStyle: no_space
35
-
36
- Lint/UnusedMethodArgument:
37
- Enabled: true
38
- Lint/UselessAssignment:
39
- Enabled: true
40
-
41
- Metrics/MethodLength:
42
- Enabled: true
43
- Max: 15
44
- Metrics/ClassLength:
45
- Enabled: true
46
- Max: 150
47
- Metrics/ModuleLength:
48
- Max: 150
49
- Metrics/ParameterLists:
50
- Enabled: true
51
- Metrics/CyclomaticComplexity:
52
- Enabled: true
53
- Metrics/AbcSize:
54
- Enabled: true
55
- Max: 30
56
-
57
- Naming/MemoizedInstanceVariableName:
58
- Enabled: false
59
- Naming/MethodParameterName:
60
- Enabled: false
61
- Naming/FileName:
62
- Enabled: false
63
-
64
- Style/Documentation:
65
- Enabled: false
66
- Style/FrozenStringLiteralComment:
67
- Enabled: true
68
- Style/NumericLiterals:
69
- Enabled: false
70
- Style/StringLiterals:
71
- EnforcedStyle: single_quotes
72
- Style/AndOr:
73
- Enabled: true
74
- Style/ClassCheck:
75
- Enabled: true
76
- Style/GuardClause:
77
- Enabled: true
78
- Style/OptionalBooleanParameter:
79
- Enabled: false
80
-
81
- Security/Eval:
82
- Enabled: true
83
- Security/JSONLoad:
84
- Enabled: true
85
- Security/YAMLLoad:
86
- Enabled: true
87
-
88
- RSpec/NestedGroups:
89
- Max: 4
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.5-dev
data/.simplecov DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- SimpleCov.start do
4
- enable_coverage :branch
5
- primary_coverage :branch
6
-
7
- add_filter '/spec'
8
- end
data/Gemfile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
- gemspec
5
-
6
- gem 'dotenv', '>= 3.1.4'
7
- gem 'irb', '>= 1.15.2'
8
- gem 'rake', '>= 13.2.1'
9
- gem 'rspec', '>= 3.13.0'
10
- gem 'rubocop', '>= 1.66.1'
11
- gem 'rubocop-rspec', '>= 3.1.0'
12
- gem 'simplecov', '>= 0.22.0'