usps-imis-api 0.11.32 → 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 (73) 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 -211
  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/error/response.rb +75 -0
  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 -33
  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 -98
  38. data/bin/imis +0 -8
  39. data/lib/usps/imis/base_data.rb +0 -68
  40. data/lib/usps/imis/blank_object.rb +0 -62
  41. data/lib/usps/imis/business_object.rb +0 -230
  42. data/lib/usps/imis/command_line/interface.rb +0 -165
  43. data/lib/usps/imis/command_line/options_parser.rb +0 -139
  44. data/lib/usps/imis/command_line/performers.rb +0 -80
  45. data/lib/usps/imis/command_line.rb +0 -15
  46. data/lib/usps/imis/data.rb +0 -60
  47. data/lib/usps/imis/error.rb +0 -55
  48. data/lib/usps/imis/errors/api_error.rb +0 -11
  49. data/lib/usps/imis/errors/command_line_error.rb +0 -11
  50. data/lib/usps/imis/errors/config_error.rb +0 -11
  51. data/lib/usps/imis/errors/locked_id_error.rb +0 -15
  52. data/lib/usps/imis/errors/mapper_error.rb +0 -29
  53. data/lib/usps/imis/errors/missing_id_error.rb +0 -15
  54. data/lib/usps/imis/errors/not_found_error.rb +0 -11
  55. data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
  56. data/lib/usps/imis/errors/response_error.rb +0 -104
  57. data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
  58. data/lib/usps/imis/logger.rb +0 -19
  59. data/lib/usps/imis/logger_formatter.rb +0 -32
  60. data/lib/usps/imis/logger_helpers.rb +0 -20
  61. data/lib/usps/imis/mocks/business_object.rb +0 -47
  62. data/lib/usps/imis/mocks.rb +0 -11
  63. data/lib/usps/imis/panels/base_panel.rb +0 -125
  64. data/lib/usps/imis/panels/education.rb +0 -29
  65. data/lib/usps/imis/panels/vsc.rb +0 -28
  66. data/lib/usps/imis/panels.rb +0 -25
  67. data/lib/usps/imis/party_data.rb +0 -93
  68. data/lib/usps/imis/properties.rb +0 -60
  69. data/lib/usps/imis/query.rb +0 -153
  70. data/lib/usps/imis/requests.rb +0 -68
  71. data/spec/support/usps/vcr/config.rb +0 -47
  72. data/spec/support/usps/vcr/filters.rb +0 -89
  73. data/spec/support/usps/vcr.rb +0 -8
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- # Base error class for all internal exceptions
6
- #
7
- class Error < StandardError
8
- # Additional call-specific metadata to pass through to Bugsnag
9
- #
10
- attr_accessor :metadata
11
-
12
- # A new instance of +ApiError+
13
- #
14
- # @param message [String] The base exception message
15
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
16
- #
17
- def initialize(message, metadata = {})
18
- super(message)
19
- @metadata = metadata
20
-
21
- Imis.logger(self.class.name).multiline(self.message, level: :error)
22
- end
23
-
24
- # Additional metadata to include in Bugsnag reports
25
- #
26
- # Can include fields at the top level, which will be shows on the custom tab
27
- #
28
- # Can include fields nested under a top-level key, which will be shown on a tab with the
29
- # top-level key as its name
30
- #
31
- # @return [Hash]
32
- #
33
- def bugsnag_meta_data
34
- metadata == {} ? {} : base_metadata
35
- end
36
-
37
- private
38
-
39
- def base_metadata
40
- { api: metadata }
41
- end
42
- end
43
- end
44
- end
45
-
46
- require_relative 'errors/api_error'
47
- require_relative 'errors/config_error'
48
- require_relative 'errors/locked_id_error'
49
- require_relative 'errors/mapper_error'
50
- require_relative 'errors/missing_id_error'
51
- require_relative 'errors/not_found_error'
52
- require_relative 'errors/response_error'
53
- require_relative 'errors/panel_unimplemented_error'
54
- require_relative 'errors/unexpected_property_type_error'
55
- require_relative 'errors/command_line_error'
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Generic exception raised from within the gem
7
- #
8
- class ApiError < 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 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 or Record ID while it is locked
7
- #
8
- class LockedIdError < Error
9
- def initialize = super(message)
10
-
11
- def message = 'Cannot change iMIS or Record 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,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Usps
4
- module Imis
5
- module Errors
6
- # Exception raised due to receiving an error response from the API
7
- #
8
- class ResponseError < Error
9
- # [Net::HTTPResponse] The response received from the API
10
- #
11
- attr_reader :response
12
-
13
- # [Hash] Additional call-specific metadata to pass through to Bugsnag
14
- #
15
- attr_accessor :metadata
16
-
17
- # Create a new instance of +ResponseError+ from an API response
18
- #
19
- # @param response [Net::HTTPResponse] The response received from the API
20
- #
21
- def self.from(response) = new(response)
22
-
23
- # Create a new instance of +ResponseError+
24
- #
25
- # @param response [Net::HTTPResponse] The response received from the API
26
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
27
- #
28
- def initialize(response, metadata = {})
29
- @response = response
30
- super(message, metadata)
31
- end
32
-
33
- # Additional metadata to include in Bugsnag reports
34
- #
35
- # Can include fields at the top level, which will be shows on the +custom+ tab
36
- #
37
- # Can include fields nested under a top-level key, which will be shown on a tab with the
38
- # top-level key as its name
39
- #
40
- # @return [Hash]
41
- #
42
- def bugsnag_meta_data
43
- base_metadata.tap { it[:api].merge!(metadata) }
44
- end
45
-
46
- # Auto-formatted exception message, based on the provided API response
47
- #
48
- # @return [String] The exception message
49
- #
50
- def message
51
- [
52
- "[#{status.to_s.upcase}] The iMIS API returned an error.",
53
- (metadata.inspect if metadata?),
54
- body
55
- ].compact.join("\n")
56
- end
57
-
58
- private
59
-
60
- def base_metadata
61
- { api: { status:, body: } }
62
- end
63
-
64
- def metadata? = metadata != {} && !metadata.nil?
65
-
66
- def status
67
- @status ||=
68
- case response.code
69
- when '400'
70
- :bad_request
71
- when '401'
72
- :unauthorized # RequestVerificationToken invalid
73
- when '404'
74
- :not_found
75
- when '422'
76
- :unprocessable_entity # validation error
77
- when /^50\d$/
78
- :internal_server_error # error within iMIS
79
- else
80
- response.code
81
- end
82
- end
83
-
84
- def response_body
85
- @response_body ||= JSON.parse(response.body)
86
- rescue StandardError
87
- @response_body ||= response.body
88
- end
89
-
90
- def body
91
- return response_body unless response_body.is_a?(Hash)
92
-
93
- case response_body['error']
94
- when 'invalid_grant'
95
- response_body['error_description']
96
- else
97
- # Unknown error type: just use the raw response
98
- JSON.pretty_generate(response.body)
99
- end
100
- end
101
- end
102
- end
103
- end
104
- 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,20 +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, level: :debug)
9
- string.split("\n").each { public_send(level, it) }
10
- end
11
-
12
- def json(data)
13
- hash = data.is_a?(String) ? JSON.parse(data) : data
14
- tagged('JSON') { multiline(JSON.pretty_generate(hash)) }
15
- rescue StandardError
16
- multiline(data)
17
- end
18
- end
19
- end
20
- 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,125 +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, record_id: nil)
18
- @api = api || Api.new
19
- @api.imis_id = imis_id if imis_id
20
- @api.record_id = record_id if record_id
21
- @logger ||= Imis.logger('Panel')
22
- end
23
-
24
- # Get a specific object from the Panel
25
- #
26
- # If +fields+ is provided, will return only those field values
27
- #
28
- # @param fields [String] Field names to return
29
- #
30
- # @param ordinal [Integer] The ordinal identifier for the desired object
31
- #
32
- # @return [Usps::Imis::Data, Array<Usps::Imis::Data>] Response data from the API
33
- #
34
- def get(ordinal, *fields) = api.on(business_object_name, ordinal:).get(*fields)
35
- alias read get
36
-
37
- # Get a single named field from a Panel for the current member
38
- #
39
- # @param ordinal [Integer] The ordinal identifier for the desired object
40
- # @param field [String] Field name to return
41
- #
42
- # @return Response data field value from the API
43
- #
44
- def get_field(ordinal, field) = api.on(business_object_name, ordinal:).get_field(field)
45
- alias fetch get_field
46
- alias [] get_field
47
-
48
- # Get named fields from a Panel for the current member
49
- #
50
- # @param ordinal [Integer] The ordinal identifier for the desired object
51
- # @param fields [Array<String>] Field names to return
52
- #
53
- # @return [Array] Response data from the API
54
- #
55
- def get_fields(ordinal, *fields) = api.on(business_object_name, ordinal:).get_fields(*fields)
56
- alias fetch_all get_fields
57
-
58
- # Update a single named field on a business object for the current member
59
- #
60
- # @param ordinal [Integer] The ordinal identifier for the desired object
61
- # @param field [String] Name of the field
62
- # @param value Value of the field
63
- #
64
- # @return [Usps::Imis::Data] Response data from the API
65
- #
66
- def put_field(ordinal, field, value) = api.on(business_object_name, ordinal:).put_field(field, value)
67
- alias []= put_field
68
-
69
- # Update only specific fields on a Panel for the current member
70
- #
71
- # @param ordinal [Integer] The ordinal identifier for the desired object
72
- # @param fields [Hash] Conforms to pattern +{ field_key => value }+
73
- #
74
- # @return [Usps::Imis::Data] Response data from the API
75
- #
76
- def put_fields(ordinal, fields) = api.on(business_object_name, ordinal:).put_fields(fields)
77
- alias patch put_fields
78
-
79
- # Update an existing object in the Panel
80
- #
81
- # @param data [Hash] The record data for the desired object -- including the required
82
- # +ordinal+ identifier
83
- #
84
- # @return [Usps::Imis::Data] Response data from the API
85
- #
86
- def put(data) = api.on(business_object_name, ordinal: data[:ordinal]).put(payload(data))
87
- alias update put
88
-
89
- # Create a new object in the Panel
90
- #
91
- # @param data [Hash] The record data for the desired object
92
- #
93
- # @return [Usps::Imis::Data] Response data from the API
94
- #
95
- def post(data) = api.on(business_object_name).post(payload(data))
96
- alias create post
97
-
98
- # Remove a specific object from the Panel
99
- #
100
- # @param ordinal [Integer] The ordinal identifier for the desired object
101
- #
102
- # @return [true] Only on success response (i.e. blank string from the API)
103
- #
104
- def delete(ordinal) = api.on(business_object_name, ordinal:).delete
105
- alias destroy delete
106
-
107
- def business_object_name
108
- raise Errors::PanelUnimplementedError.from(self.class.name, 'business_object_name')
109
- end
110
-
111
- # Ruby 3.5 instance variable filter
112
- #
113
- def instance_variables_to_inspect = instance_variables - %i[@api @logger]
114
-
115
- private
116
-
117
- def payload(_data)
118
- raise Errors::PanelUnimplementedError.from(self.class.name, 'payload(data)')
119
- end
120
-
121
- def build_payload(data, &) = BlankObject.new(self, data[:ordinal]).build(&)
122
- end
123
- end
124
- end
125
- end
@@ -1,29 +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
- def business_object_name = 'ABC_ASC_Educ'
10
-
11
- private
12
-
13
- def payload(data)
14
- build_payload(data) do |props|
15
- props.add 'ABC_EDUC_THRU_DATE', data[:thru_date] || '0001-01-01T00:00:00'
16
- props.add 'ABC_ECertificate', data[:certificate]
17
- props.add 'ABC_Educ_Description', data[:description]
18
- props.add 'ABC_Educ_Effective_Date', data[:effective_date]
19
- props.add 'ABC_Educ_Source_System', data[:source]
20
- props.add 'ABC_Educ_Transaction_Date', Time.now
21
- props.add 'ABC_Other_Code', data[:code]
22
- props.add 'ABC_Product_Code', data[:type_code]
23
- props.add 'ABC_TYPE', data[:abc_type_code] || 'EDUC'
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,28 +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
- def business_object_name = 'ABC_ASC_Vessel_Safety_Checks'
10
-
11
- private
12
-
13
- def payload(data)
14
- build_payload(data) do |props|
15
- props.add 'Source_System', 'Manual ITCom Entry'
16
- props.add 'ABC_ECertificate', data[:certificate]
17
- props.add 'Activity_Type', 'VSC'
18
- props.add 'Description', 'Vessel Safety Checks'
19
- props.add 'Effective_Date', "#{data[:year]}-12-01T00:00:00"
20
- props.add 'Quantity', data[:count]
21
- props.add 'Thru_Date', "#{data[:year]}-12-31T00:00:00"
22
- props.add 'Transaction_Date', Time.now
23
- end
24
- end
25
- end
26
- end
27
- end
28
- 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