usps-imis-api 0.11.22 → 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 (68) 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 -94
  38. data/bin/imis +0 -8
  39. data/lib/usps/imis/business_object.rb +0 -209
  40. data/lib/usps/imis/command_line/interface.rb +0 -205
  41. data/lib/usps/imis/command_line/options_parser.rb +0 -132
  42. data/lib/usps/imis/command_line.rb +0 -15
  43. data/lib/usps/imis/data.rb +0 -76
  44. data/lib/usps/imis/error.rb +0 -55
  45. data/lib/usps/imis/errors/api_error.rb +0 -11
  46. data/lib/usps/imis/errors/command_line_error.rb +0 -11
  47. data/lib/usps/imis/errors/config_error.rb +0 -11
  48. data/lib/usps/imis/errors/locked_id_error.rb +0 -15
  49. data/lib/usps/imis/errors/mapper_error.rb +0 -29
  50. data/lib/usps/imis/errors/missing_id_error.rb +0 -15
  51. data/lib/usps/imis/errors/not_found_error.rb +0 -11
  52. data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
  53. data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
  54. data/lib/usps/imis/logger.rb +0 -19
  55. data/lib/usps/imis/logger_formatter.rb +0 -32
  56. data/lib/usps/imis/logger_helpers.rb +0 -17
  57. data/lib/usps/imis/mocks/business_object.rb +0 -47
  58. data/lib/usps/imis/mocks.rb +0 -11
  59. data/lib/usps/imis/panels/base_panel.rb +0 -158
  60. data/lib/usps/imis/panels/education.rb +0 -33
  61. data/lib/usps/imis/panels/vsc.rb +0 -32
  62. data/lib/usps/imis/panels.rb +0 -25
  63. data/lib/usps/imis/properties.rb +0 -50
  64. data/lib/usps/imis/query.rb +0 -153
  65. data/lib/usps/imis/requests.rb +0 -68
  66. data/spec/support/usps/vcr/config.rb +0 -47
  67. data/spec/support/usps/vcr/filters.rb +0 -89
  68. data/spec/support/usps/vcr.rb +0 -8
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised by the command line interface
7
- #
8
- class CommandLineError < Error; end
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised for invalid configuration
7
- #
8
- class ConfigError < Error; end
9
- end
10
- end
11
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised when attempting to change the iMIS ID while it is locked
7
- #
8
- class LockedIdError < Error
9
- def initialize = super(message)
10
-
11
- def message = 'Cannot change iMIS ID while locked'
12
- end
13
- end
14
- end
15
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised by a +Mapper+
7
- #
8
- class MapperError < Error
9
- # Create a new instance of +MapperError+
10
- #
11
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
12
- #
13
- def initialize(metadata = {})
14
- @metadata = metadata
15
- super(message, metadata)
16
- end
17
-
18
- # Exception message including the unrecognized field
19
- #
20
- def message
21
- <<~MESSAGE.chomp
22
- Mapper does not recognize field: "#{metadata[:field_key]}".
23
- Please report what data you are attempting to work with to ITCom leadership.
24
- MESSAGE
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,15 +0,0 @@
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
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised when the requested object cannot be found
7
- #
8
- class NotFoundError < Error; end
9
- end
10
- end
11
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised when a panel is missing required method definitions
7
- #
8
- class PanelUnimplementedError < Error
9
- # Create a new instance of +PanelUnimplementedError+ from the class name and missing method
10
- #
11
- # @param class_name [String] Name of the Panel class that is missing a method definition
12
- # @param method [String] Method definition that is not defined on the Panel
13
- #
14
- def self.from(class_name, method) = new(class_name, method)
15
-
16
- # Create a new instance of +PanelUnimplementedError+
17
- #
18
- # @param class_name [String] Name of the Panel class that is missing a method definition
19
- # @param method [String] Method definition that is not defined on the Panel
20
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
21
- #
22
- def initialize(class_name, method, metadata = {})
23
- @class_name = class_name
24
- @method = method
25
- super(message, metadata)
26
- end
27
-
28
- # Exception message including the undefined method
29
- #
30
- def message = "#{@class_name} must implement ##{@method}"
31
- end
32
- end
33
- end
34
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised when attempting to wrap an unexpected property type
7
- #
8
- class UnexpectedPropertyTypeError < Error
9
- # Create a new instance of +UnexpectedPropertyTypeError+ from an unexpected value
10
- #
11
- # @param value Unexpected value
12
- #
13
- def self.from(value) = new(value)
14
-
15
- # Create a new instance of +UnexpectedPropertyTypeError+
16
- #
17
- # @param value Unexpected value
18
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
19
- #
20
- def initialize(value, metadata = {})
21
- @value = value
22
- super(message, metadata)
23
- end
24
-
25
- # Exception message including the undefined method
26
- #
27
- def message = "Unexpected property type: #{@value.inspect}"
28
- end
29
- end
30
- end
31
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'logger_formatter'
4
- require_relative 'logger_helpers'
5
-
6
- module Usps
7
- module Imis
8
- # Formatted logger with additional helpers
9
- #
10
- class Logger < ::Logger
11
- include LoggerHelpers
12
-
13
- def initialize(...)
14
- super
15
- self.formatter = LoggerFormatter.new
16
- end
17
- end
18
- end
19
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # Formats log statements
6
- #
7
- class LoggerFormatter < ::Logger::Formatter
8
- include ::ActiveSupport::TaggedLogging::Formatter
9
-
10
- def call(severity, time, _progname, message)
11
- log_chunks = [
12
- format('%-5s', severity.to_s),
13
- "[#{$PROCESS_ID}]",
14
- "[#{time.strftime('%Y-%m-%d %H:%M:%S %Z')}]",
15
- 'iMIS Ruby API',
16
- '|',
17
- formatted_tags,
18
- '|',
19
- message.to_s.sub(/^#{Regexp.escape(tags_text)}/, '')
20
- ]
21
- "#{log_chunks.join(' ')}\n"
22
- end
23
-
24
- private
25
-
26
- def formatted_tags
27
- tags = current_tags || []
28
- tags&.any? ? tags.join(' | ') : ''
29
- end
30
- end
31
- end
32
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # Formatted logger helpers
6
- #
7
- module LoggerHelpers
8
- def multiline(string)
9
- string.split("\n").each { |line| debug(line) }
10
- end
11
-
12
- def json(data)
13
- tagged('JSON') { multiline(JSON.pretty_generate(data)) }
14
- end
15
- end
16
- end
17
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Mocks
6
- # Mock data response for testing
7
- #
8
- class BusinessObject
9
- attr_reader :fields
10
-
11
- def initialize(**fields)
12
- @fields = fields.transform_keys(&:to_s)
13
- end
14
-
15
- def get
16
- Usps::Imis::Properties.build do |props|
17
- fields.each { |name, value| props.add(name, value) }
18
- end
19
- end
20
- alias read get
21
-
22
- def get_field(name) = fields[name]
23
- alias fetch get_field
24
- alias [] get_field
25
-
26
- def get_fields(*field_names) = field_names.map { fields[it] }
27
- alias fetch_all get_fields
28
-
29
- def put_fields(data)
30
- Usps::Imis::Properties.build do |props|
31
- fields.merge(data.transform_keys(&:to_s)).each { |name, value| props.add(name, value) }
32
- end
33
- end
34
- alias patch put_fields
35
-
36
- def put(data) = data
37
- alias update put
38
-
39
- def post(data) = data
40
- alias create post
41
-
42
- def delete = ''
43
- alias destroy delete
44
- end
45
- end
46
- end
47
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'mocks/business_object'
4
-
5
- module Usps
6
- module Imis
7
- # Namespace for all Mocks
8
- #
9
- module Mocks; end
10
- end
11
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Panels
6
- # Base class for configuring Panels
7
- #
8
- class BasePanel
9
- # The parent +Api+ object
10
- #
11
- attr_reader :api
12
-
13
- # Tagged logger
14
- #
15
- attr_reader :logger
16
-
17
- def initialize(api = nil, imis_id: nil)
18
- @api = api || Api.new
19
- @api.imis_id = imis_id if imis_id
20
- @logger ||= Imis.logger('Panel')
21
- end
22
-
23
- # Get a specific object from the Panel
24
- #
25
- # If +fields+ is provided, will return only those field values
26
- #
27
- # @param fields [String] Field names to return
28
- #
29
- # @param ordinal [Integer] The ordinal identifier for the desired object
30
- #
31
- # @return [Usps::Imis::Data, Array<Usps::Imis::Data>] Response data from the API
32
- #
33
- def get(ordinal, *fields) = api.on(business_object, ordinal:).get(*fields)
34
- alias read get
35
-
36
- # Get a single named field from a Panel for the current member
37
- #
38
- # @param ordinal [Integer] The ordinal identifier for the desired object
39
- # @param field [String] Field name to return
40
- #
41
- # @return Response data field value from the API
42
- #
43
- def get_field(ordinal, field) = api.on(business_object, ordinal:).get_field(field)
44
- alias fetch get_field
45
- alias [] get_field
46
-
47
- # Get named fields from a Panel for the current member
48
- #
49
- # @param ordinal [Integer] The ordinal identifier for the desired object
50
- # @param fields [Array<String>] Field names to return
51
- #
52
- # @return [Array] Response data from the API
53
- #
54
- def get_fields(ordinal, *fields) = api.on(business_object, ordinal:).get_fields(*fields)
55
- alias fetch_all get_fields
56
-
57
- # Update a single named field on a business object for the current member
58
- #
59
- # @param ordinal [Integer] The ordinal identifier for the desired object
60
- # @param field [String] Name of the field
61
- # @param value Value of the field
62
- #
63
- # @return [Usps::Imis::Data] Response data from the API
64
- #
65
- def put_field(ordinal, field, value) = api.on(business_object, ordinal:).put_field(field, value)
66
- alias []= put_field
67
-
68
- # Update only specific fields on a Panel for the current member
69
- #
70
- # @param ordinal [Integer] The ordinal identifier for the desired object
71
- # @param fields [Hash] Conforms to pattern +{ field_key => value }+
72
- #
73
- # @return [Usps::Imis::Data] Response data from the API
74
- #
75
- def put_fields(ordinal, fields) = api.on(business_object, ordinal:).put_fields(fields)
76
- alias patch put_fields
77
-
78
- # Update an existing object in the Panel
79
- #
80
- # @param data [Hash] The record data for the desired object -- including the required
81
- # +ordinal+ identifier
82
- #
83
- # @return [Usps::Imis::Data] Response data from the API
84
- #
85
- def put(data) = api.on(business_object, ordinal: data[:ordinal]).put(payload(data))
86
- alias update put
87
-
88
- # Create a new object in the Panel
89
- #
90
- # @param data [Hash] The record data for the desired object
91
- #
92
- # @return [Usps::Imis::Data] Response data from the API
93
- #
94
- def post(data) = api.on(business_object).post(payload(data))
95
- alias create post
96
-
97
- # Remove a specific object from the Panel
98
- #
99
- # @param ordinal [Integer] The ordinal identifier for the desired object
100
- #
101
- # @return [true] Only on success response (i.e. blank string from the API)
102
- #
103
- def delete(ordinal) = api.on(business_object, ordinal:).delete
104
- alias destroy delete
105
-
106
- # Ruby 3.5 instance variable filter
107
- #
108
- def instance_variables_to_inspect = instance_variables - %i[@api @logger]
109
-
110
- private
111
-
112
- def business_object
113
- raise Errors::PanelUnimplementedError.from(self.class.name, 'business_object')
114
- end
115
-
116
- def payload(_data)
117
- raise Errors::PanelUnimplementedError.from(self.class.name, 'payload(data)')
118
- end
119
-
120
- def payload_header(data)
121
- {
122
- '$type' => 'Asi.Soa.Core.DataContracts.GenericEntityData, Asi.Contracts',
123
- 'EntityTypeName' => business_object,
124
- 'PrimaryParentEntityTypeName' => 'Party',
125
- 'Identity' => identity(data[:ordinal]),
126
- 'PrimaryParentIdentity' => primary_parent_identity
127
- }
128
- end
129
-
130
- def identity(ordinal = nil)
131
- {
132
- '$type' => 'Asi.Soa.Core.DataContracts.IdentityData, Asi.Contracts',
133
- 'EntityTypeName' => business_object,
134
- 'IdentityElements' => {
135
- '$type' => identity_type,
136
- '$values' => [api.imis_id.to_s, ordinal&.to_s].compact
137
- }
138
- }
139
- end
140
-
141
- def primary_parent_identity
142
- {
143
- '$type' => 'Asi.Soa.Core.DataContracts.IdentityData, Asi.Contracts',
144
- 'EntityTypeName' => 'Party',
145
- 'IdentityElements' => {
146
- '$type' => identity_type,
147
- '$values' => [api.imis_id.to_s]
148
- }
149
- }
150
- end
151
-
152
- def identity_type = 'System.Collections.ObjectModel.Collection`1[[System.String, mscorlib]], mscorlib'
153
-
154
- def build_payload(data, &) = payload_header(data).merge(Properties.build(&))
155
- end
156
- end
157
- end
158
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Panels
6
- # Panel for accessing the Educational completions business object
7
- #
8
- class Education < BasePanel
9
- private
10
-
11
- def business_object
12
- 'ABC_ASC_Educ'
13
- end
14
-
15
- def payload(data)
16
- build_payload(data) do |props|
17
- props.add 'ID', api.imis_id.to_s
18
- props.add 'Ordinal', data[:ordinal] if data[:ordinal]
19
- props.add 'ABC_EDUC_THRU_DATE', data[:thru_date] || '0001-01-01T00:00:00'
20
- props.add 'ABC_ECertificate', data[:certificate]
21
- props.add 'ABC_Educ_Description', data[:description]
22
- props.add 'ABC_Educ_Effective_Date', data[:effective_date]
23
- props.add 'ABC_Educ_Source_System', data[:source]
24
- props.add 'ABC_Educ_Transaction_Date', Time.now
25
- props.add 'ABC_Other_Code', data[:code]
26
- props.add 'ABC_Product_Code', data[:type_code]
27
- props.add 'ABC_TYPE', data[:abc_type_code] || 'EDUC'
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Panels
6
- # Panel for accessing the annual VSC completed counts business object
7
- #
8
- class Vsc < BasePanel
9
- private
10
-
11
- def business_object
12
- 'ABC_ASC_Vessel_Safety_Checks'
13
- end
14
-
15
- def payload(data)
16
- build_payload(data) do |props|
17
- props.add 'ID', api.imis_id.to_s
18
- props.add 'Ordinal', data[:ordinal] if data[:ordinal]
19
- props.add 'Source_System', 'Manual ITCom Entry'
20
- props.add 'ABC_ECertificate', data[:certificate]
21
- props.add 'Activity_Type', 'VSC'
22
- props.add 'Description', 'Vessel Safety Checks'
23
- props.add 'Effective_Date', "#{data[:year]}-12-01T00:00:00"
24
- props.add 'Quantity', data[:count]
25
- props.add 'Thru_Date', "#{data[:year]}-12-31T00:00:00"
26
- props.add 'Transaction_Date', Time.now
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'panels/base_panel'
4
- require_relative 'panels/vsc'
5
- require_relative 'panels/education'
6
-
7
- module Usps
8
- module Imis
9
- # Namespace for all Panels
10
- #
11
- module Panels
12
- # Convenience accessor for available Panel objects
13
- #
14
- # @param api [Api] Parent to use for making requests
15
- #
16
- def self.all(api = Api.new)
17
- panels = constants.reject { it == :BasePanel }
18
-
19
- Struct
20
- .new(*panels.map { it.to_s.underscore.to_sym })
21
- .new(*panels.map { const_get(it).new(api) })
22
- end
23
- end
24
- end
25
- 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