usps-imis-api 0.2.1 → 0.3.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.
data/lib/usps/imis.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core dependencies
4
+ require 'net/https'
5
+ require 'json'
6
+ require 'time'
7
+ require 'cgi'
8
+
9
+ # Extensions
10
+ require 'ext/hash' unless defined?(Rails)
11
+
12
+ # Internal requires
13
+ require_relative 'imis/config'
14
+ require_relative 'imis/error/api'
15
+ require_relative 'imis/error/mapper'
16
+ require_relative 'imis/api'
17
+ require_relative 'imis/mapper'
18
+ require_relative 'imis/panel/vsc'
19
+
20
+ module Usps
21
+ module Imis
22
+ class << self
23
+ def configuration
24
+ @configuration ||= Config.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration) if block_given?
29
+ configuration
30
+ end
31
+
32
+ # def mock!(value = true)
33
+ # @mock = value
34
+ # end
35
+
36
+ # def mock
37
+ # @mock || false
38
+ # end
39
+ end
40
+ end
41
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Imis::Api do
5
+ describe Usps::Imis::Api do
6
6
  let(:api) { described_class.new }
7
7
 
8
8
  describe '#imis_id_for' do
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Imis::Mapper do
6
- let(:api) { Imis::Api.new }
5
+ describe Usps::Imis::Mapper do
6
+ let(:api) { described_class.new.api }
7
7
 
8
8
  describe '#update' do
9
9
  before { api.imis_id = 31092 }
@@ -14,7 +14,7 @@ describe Imis::Mapper do
14
14
 
15
15
  it 'raises for unmapped updates' do
16
16
  expect { api.mapper.update(something: 'anything') }.to raise_error(
17
- Imis::Error::Mapper,
17
+ Usps::Imis::Error::Mapper,
18
18
  'Unrecognized field: "something". ' \
19
19
  'Please report what data you are attempting to work with to ITCom leadership.'
20
20
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Imis::Panel::Vsc do
5
+ describe Usps::Imis::Panel::Vsc do
6
6
  let(:vsc) { described_class.new }
7
7
 
8
8
  let(:details) do
data/spec/spec_helper.rb CHANGED
@@ -9,13 +9,25 @@ end
9
9
  # SimpleCov.minimum_coverage(100)
10
10
 
11
11
  require 'dotenv/load'
12
- require 'usps-imis-api'
12
+ require 'usps/imis'
13
13
 
14
14
  ENV['TESTING'] = 'true'
15
15
 
16
16
  RSpec.configure do |config|
17
+ # Enable flags like --only-failures and --next-failure
18
+ config.example_status_persistence_file_path = 'tmp/.rspec_status'
19
+
20
+ # Disable RSpec exposing methods globally on `Module` and `main`
21
+ config.disable_monkey_patching!
22
+
23
+ config.expose_dsl_globally = true
24
+
25
+ config.expect_with :rspec do |c|
26
+ c.syntax = :expect
27
+ end
28
+
17
29
  config.before(:suite) do
18
- Imis.configure do |imis_config|
30
+ Usps::Imis.configure do |imis_config|
19
31
  imis_config.environment = :development
20
32
  imis_config.imis_id_query_name = ENV.fetch('IMIS_ID_QUERY_NAME', '')
21
33
 
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lib/usps/imis/version'
4
+
3
5
  Gem::Specification.new do |s|
4
6
  s.name = 'usps-imis-api'
5
- s.version = '0.2.1'
7
+ s.version = Usps::Imis::VERSION
6
8
  s.summary = 'iMIS API Wrapper'
7
9
  s.description = 'A wrapper for the iMIS API.'
8
10
  s.homepage = 'http://rubygems.org/gems/usps-imis-api'
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Fiander
@@ -23,18 +23,22 @@ files:
23
23
  - ".ruby-version"
24
24
  - Gemfile
25
25
  - Gemfile.lock
26
+ - Rakefile
26
27
  - Readme.md
28
+ - bin/console
29
+ - bin/setup
27
30
  - lib/ext/hash.rb
28
- - lib/imis/api.rb
29
- - lib/imis/config.rb
30
- - lib/imis/error/api.rb
31
- - lib/imis/error/mapper.rb
32
- - lib/imis/mapper.rb
33
- - lib/imis/panel/vsc.rb
34
- - lib/usps-imis-api.rb
35
- - spec/lib/imis/api_spec.rb
36
- - spec/lib/imis/mapper_spec.rb
37
- - spec/lib/imis/panel/vsc_spec.rb
31
+ - lib/usps/imis.rb
32
+ - lib/usps/imis/api.rb
33
+ - lib/usps/imis/config.rb
34
+ - lib/usps/imis/error/api.rb
35
+ - lib/usps/imis/error/mapper.rb
36
+ - lib/usps/imis/mapper.rb
37
+ - lib/usps/imis/panel/vsc.rb
38
+ - lib/usps/imis/version.rb
39
+ - spec/lib/usps/imis/api_spec.rb
40
+ - spec/lib/usps/imis/mapper_spec.rb
41
+ - spec/lib/usps/imis/panel/vsc_spec.rb
38
42
  - spec/spec_helper.rb
39
43
  - usps-imis-api.gemspec
40
44
  homepage: http://rubygems.org/gems/usps-imis-api
data/lib/imis/api.rb DELETED
@@ -1,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- class Api
5
- AUTHENTICATION_PATH = 'Token'
6
- API_PATH = 'api'
7
- QUERY_PATH = 'api/Query'
8
- PANELS = Struct.new(:vsc)
9
-
10
- attr_reader :token, :token_expiration, :imis_id
11
-
12
- def initialize(skip_authentication: false)
13
- authenticate unless skip_authentication
14
- end
15
-
16
- # Manually set the current ID, if you already have it for a given member
17
- #
18
- def imis_id=(id)
19
- @imis_id = id.to_s
20
- end
21
-
22
- # Convert a member's certificate number into an iMIS ID number
23
- #
24
- def imis_id_for(certificate)
25
- result = query(Imis.configuration.imis_id_query_name, { certificate: certificate })
26
- @imis_id = result['Items']['$values'][0]['ID']
27
- end
28
-
29
- # Run requests as DSL, with specific iMIS ID only maintained for this scope
30
- #
31
- # This should be used with methods that do not change the value of `imis_id`
32
- #
33
- def with(id, &block)
34
- old_id = imis_id
35
- self.imis_id = id
36
- instance_eval(&block)
37
- ensure
38
- self.imis_id = old_id
39
- end
40
-
41
- # Get a business object for the current member
42
- #
43
- def get(business_object_name, url_id: nil)
44
- uri = uri_for(business_object_name, url_id: url_id)
45
- request = Net::HTTP::Get.new(uri)
46
- result = submit(uri, authorize(request))
47
- JSON.parse(result.body)
48
- end
49
-
50
- # Update only specific fields on a business object for the current member
51
- #
52
- # fields - hash of shape: { field_name => new_value }
53
- #
54
- def put_fields(business_object_name, fields, url_id: nil)
55
- updated = filter_fields(business_object_name, fields)
56
- put(business_object_name, updated, url_id: url_id)
57
- end
58
-
59
- # Update a business object for the current member
60
- #
61
- def put(business_object_name, body, url_id: nil)
62
- uri = uri_for(business_object_name, url_id: url_id)
63
- request = Net::HTTP::Put.new(uri)
64
- request.body = JSON.dump(body)
65
- result = submit(uri, authorize(request))
66
- JSON.parse(result.body)
67
- end
68
-
69
- # Create a business object for the current member
70
- #
71
- def post(business_object_name, body, url_id: nil)
72
- uri = uri_for(business_object_name, url_id: url_id)
73
- request = Net::HTTP::Post.new(uri)
74
- request.body = JSON.dump(body)
75
- result = submit(uri, authorize(request))
76
- JSON.parse(result.body)
77
- end
78
-
79
- # Remove a business object for the current member
80
- #
81
- # Returns empty string on success
82
- #
83
- def delete(business_object_name, url_id: nil)
84
- uri = uri_for(business_object_name, url_id: url_id)
85
- request = Net::HTTP::Delete.new(uri)
86
- result = submit(uri, authorize(request))
87
- result.body
88
- end
89
-
90
- # Run an IQA Query
91
- #
92
- # query_name - the full path of the query in IQA, e.g. `$/_ABC/Fiander/iMIS_ID`
93
- # query_params - hash of shape: { param_name => param_value }
94
- #
95
- def query(query_name, query_params = {})
96
- query_params[:QueryName] = query_name
97
- path = "#{QUERY_PATH}?#{query_params.to_query}"
98
- uri = URI(File.join(imis_hostname, path))
99
- request = Net::HTTP::Get.new(uri)
100
- result = submit(uri, authorize(request))
101
- JSON.parse(result.body)
102
- end
103
-
104
- def mapper
105
- @mapper ||= Mapper.new(self)
106
- end
107
-
108
- def panels
109
- @panels ||= PANELS.new(
110
- Panel::Vsc.new(self)
111
- )
112
- end
113
-
114
- def update(data)
115
- mapper.update(data)
116
- end
117
-
118
- private
119
-
120
- def client(uri)
121
- Net::HTTP.new(uri.host, uri.port).tap do |http|
122
- http.use_ssl = true
123
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
124
- end
125
- end
126
-
127
- def imis_hostname
128
- Imis.configuration.hostname
129
- end
130
-
131
- # Authorize a request prior to submitting
132
- #
133
- # If the current token is missing/expired, request a new one
134
- #
135
- def authorize(request)
136
- authenticate if token_expiration < Time.now
137
- request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
138
- end
139
-
140
- # Construct a business object API endpoint address
141
- #
142
- def uri_for(business_object_name, url_id: nil)
143
- url_id ||= imis_id
144
- url_id = CGI.escape(url_id)
145
- URI(File.join(imis_hostname, "#{API_PATH}/#{business_object_name}/#{url_id}"))
146
- end
147
-
148
- def submit(uri, request)
149
- client(uri).request(request).tap do |result|
150
- raise Error::Api.from(result) unless result.is_a?(Net::HTTPSuccess)
151
- end
152
- end
153
-
154
- # Authenticate to the iMIS API, and store the access token and expiration time
155
- #
156
- def authenticate
157
- uri = URI(File.join(imis_hostname, AUTHENTICATION_PATH))
158
- req = Net::HTTP::Post.new(uri)
159
- authentication_data = {
160
- grant_type: 'password',
161
- username: Imis.configuration.username,
162
- password: Imis.configuration.password
163
- }
164
- req.body = URI.encode_www_form(authentication_data)
165
- result = submit(uri, req)
166
- json = JSON.parse(result.body)
167
-
168
- @token = json['access_token']
169
- @token_expiration = Time.parse(json['.expires'])
170
- end
171
-
172
- # Manually assemble the matching data structure, with fields in the correct order
173
- #
174
- def filter_fields(business_object_name, fields)
175
- existing = get(business_object_name)
176
-
177
- JSON.parse(JSON.dump(existing)).tap do |updated|
178
- # The first property is always the iMIS ID again
179
- updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
180
-
181
- # Iterate through all existing fields
182
- existing['Properties']['$values'].each do |value|
183
- next unless fields.keys.include?(value['Name'])
184
-
185
- # Strings are not wrapped in the type definition structure
186
- new_value = fields[value['Name']]
187
- if new_value.is_a?(String)
188
- value['Value'] = new_value
189
- else
190
- value['Value']['$value'] = new_value
191
- end
192
-
193
- # Add the completed field with the updated value
194
- updated['Properties']['$values'] << value
195
- end
196
- end
197
- end
198
- end
199
- end
data/lib/imis/config.rb DELETED
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- class Config
5
- IMIS_ROOT_URL_PROD = 'https://portal.americasboatingclub.org'
6
- IMIS_ROOT_URL_DEV = 'https://abcdev.imiscloud.com'
7
-
8
- attr_accessor :environment, :imis_id_query_name, :username, :password
9
-
10
- def initialize
11
- yield self if block_given?
12
- end
13
-
14
- def hostname
15
- case environment.to_sym
16
- when :production
17
- IMIS_ROOT_URL_PROD
18
- when :development
19
- IMIS_ROOT_URL_DEV
20
- else
21
- raise "Unexpected API environment: #{environment}"
22
- end
23
- end
24
- end
25
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- module Error
5
- class Api < StandardError
6
- attr_reader :response
7
- attr_accessor :metadata
8
-
9
- def self.from(response)
10
- new('The iMIS API returned an error.', response)
11
- end
12
-
13
- def initialize(message, response, metadata = {})
14
- super(message)
15
- @metadata = metadata
16
- @response = response
17
- end
18
-
19
- def bugsnag_meta_data
20
- { api: { status: status, body: body }.merge!(metadata) }
21
- end
22
-
23
- private
24
-
25
- def status
26
- @status ||=
27
- case response.code
28
- when '400'
29
- :bad_request
30
- when '401'
31
- :unauthorized # RequestVerificationToken invalid
32
- when '404'
33
- :not_found
34
- when '422'
35
- :unprocessable_entity # validation error
36
- when /^50\d$/
37
- :internal_server_error # error within iMIS
38
- end
39
- end
40
-
41
- def response_body
42
- @response_body ||= JSON.parse(response.body)
43
- rescue StandardError
44
- @response_body ||= response.body
45
- end
46
-
47
- def body
48
- return response_body unless response_body.is_a?(Hash)
49
-
50
- case response_body['error']
51
- when 'invalid_grant'
52
- response_body['error_description']
53
- else
54
- # Unknown error type: just use the raw response
55
- response.body
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- module Error
5
- class Mapper < StandardError; end
6
- end
7
- end
data/lib/imis/mapper.rb DELETED
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- class Mapper
5
- FIELD_MAPPING = {
6
- vessel_examiner: %w[ABC_ASC_Individual_Demog Vol_Vessel_Examiner],
7
- mm: %w[ABC_ASC_Individual_Demog TotMMS],
8
- mm_updated: %w[ABC_ASC_Individual_Demog MMS_Updated]
9
- }.freeze
10
-
11
- attr_reader :api
12
-
13
- def initialize(api = nil)
14
- @api = api || Api.new
15
- end
16
-
17
- # Update a member's data on multiple affected business objects by arbitrary field names
18
- # Does not require knowing which business object / iMIS-specific field name to use
19
- #
20
- # Only available for previously-mapped fields
21
- #
22
- # `data` is a hash of shape { field_key => value }
23
- #
24
- def update(data)
25
- updates = data.each_with_object({}) do |(field_key, value), hash|
26
- map_update(field_key) do |business_object_name, field|
27
- hash[business_object_name] ||= {}
28
- hash[business_object_name][field] = value
29
- end
30
- end
31
-
32
- updates.map do |business_object_name, field_updates|
33
- api.put_fields(business_object_name, field_updates)
34
- end
35
- end
36
-
37
- private
38
-
39
- def map_update(field_name)
40
- if FIELD_MAPPING.key?(field_name.to_sym)
41
- business_object_name, field = FIELD_MAPPING[field_name.to_sym]
42
- yield(business_object_name, field)
43
- else
44
- missing_mapping(field_name)
45
- end
46
- end
47
-
48
- def missing_mapping(field_name)
49
- unless ENV['TESTING']
50
- warn(
51
- "Mapper does not know how to handle field \"#{field_name}\".\n\n" \
52
- 'You can use api.put_fields(business_object_name, { field_name => value }) ' \
53
- "if you know the business object and iMIS-specific field name.\n\n"
54
- )
55
- end
56
-
57
- raise(
58
- Imis::Error::Mapper,
59
- "Unrecognized field: \"#{field_name}\". " \
60
- 'Please report what data you are attempting to work with to ITCom leadership.'
61
- )
62
- end
63
- end
64
- end
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Imis
4
- module Panel
5
- class Vsc
6
- BUSINESS_OBJECT = 'ABC_ASC_Vessel_Safety_Checks'
7
-
8
- attr_reader :api
9
-
10
- def initialize(api = nil)
11
- @api = api || Api.new
12
- end
13
-
14
- def get(ordinal)
15
- api.get(BUSINESS_OBJECT, url_id: "~#{api.imis_id}|#{ordinal}")
16
- end
17
-
18
- def create(data)
19
- api.post(BUSINESS_OBJECT, payload(data), url_id: '')
20
- end
21
-
22
- def update(data)
23
- api.put(BUSINESS_OBJECT, payload(data), url_id: "~#{api.imis_id}|#{data[:ordinal]}")
24
- end
25
-
26
- def destroy(ordinal)
27
- api.delete(BUSINESS_OBJECT, url_id: "~#{api.imis_id}|#{ordinal}")
28
- end
29
-
30
- private
31
-
32
- # rubocop:disable Metrics/MethodLength
33
- def payload(data)
34
- {
35
- '$type' => 'Asi.Soa.Core.DataContracts.GenericEntityData, Asi.Contracts',
36
- 'EntityTypeName' => 'ABC_ASC_Vessel_Safety_Checks',
37
- 'PrimaryParentEntityTypeName' => 'Party',
38
- 'Identity' => {
39
- '$type' => 'Asi.Soa.Core.DataContracts.IdentityData, Asi.Contracts',
40
- 'EntityTypeName' => 'ABC_ASC_Vessel_Safety_Checks',
41
- 'IdentityElements' => {
42
- '$type' =>
43
- 'System.Collections.ObjectModel.Collection`1[[System.String, mscorlib]], mscorlib',
44
- '$values' => [api.imis_id]
45
- }
46
- },
47
- 'PrimaryParentIdentity' => {
48
- '$type' => 'Asi.Soa.Core.DataContracts.IdentityData, Asi.Contracts',
49
- 'EntityTypeName' => 'Party',
50
- 'IdentityElements' => {
51
- '$type' =>
52
- 'System.Collections.ObjectModel.Collection`1[[System.String, mscorlib]], mscorlib',
53
- '$values' => [api.imis_id]
54
- }
55
- },
56
- 'Properties' => {
57
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyDataCollection, Asi.Contracts',
58
- '$values' => [
59
- {
60
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
61
- 'Name' => 'ID',
62
- 'Value' => api.imis_id
63
- },
64
- (
65
- if data[:ordinal]
66
- {
67
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
68
- 'Name' => 'Ordinal',
69
- 'Value' => {
70
- '$type' => 'System.Int32',
71
- '$value' => data[:ordinal]
72
- }
73
- }
74
- end
75
- ),
76
- {
77
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
78
- 'Name' => 'Source_System',
79
- 'Value' => 'Manual ITCom Entry'
80
- },
81
- {
82
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
83
- 'Name' => 'ABC_ECertificate',
84
- 'Value' => data[:certificate]
85
- },
86
- {
87
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
88
- 'Name' => 'Activity_Type',
89
- 'Value' => 'VSC'
90
- },
91
- {
92
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
93
- 'Name' => 'Description',
94
- 'Value' => 'Vessel Safety Checks'
95
- },
96
- {
97
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
98
- 'Name' => 'Effective_Date',
99
- 'Value' => "#{data[:year]}-12-01T00:00:00"
100
- },
101
- {
102
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
103
- 'Name' => 'Quantity',
104
- 'Value' => {
105
- '$type' => 'System.Int32',
106
- '$value' => data[:count]
107
- }
108
- },
109
- {
110
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
111
- 'Name' => 'Thru_Date',
112
- 'Value' => "#{data[:year]}-12-31T00:00:00"
113
- },
114
- {
115
- '$type' => 'Asi.Soa.Core.DataContracts.GenericPropertyData, Asi.Contracts',
116
- 'Name' => 'Transaction_Date',
117
- 'Value' => Time.now.strftime('%Y-%m-%dT%H:%M:%S')
118
- }
119
- ].compact
120
- }
121
- }
122
- end
123
- # rubocop:enable Metrics/MethodLength
124
- end
125
- end
126
- end
data/lib/usps-imis-api.rb DELETED
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Core dependencies
4
- require 'net/https'
5
- require 'json'
6
- require 'time'
7
- require 'cgi'
8
-
9
- # Extensions
10
- require 'ext/hash' unless defined?(Rails)
11
-
12
- # Internal requires
13
- require 'imis/config'
14
- require 'imis/error/api'
15
- require 'imis/error/mapper'
16
- require 'imis/api'
17
- require 'imis/mapper'
18
- require 'imis/panel/vsc'
19
-
20
- module Imis
21
- class << self
22
- def configuration
23
- @configuration ||= Imis::Config.new
24
- end
25
-
26
- def configure
27
- yield(configuration) if block_given?
28
- configuration
29
- end
30
-
31
- # def mock!(value = true)
32
- # @mock = value
33
- # end
34
-
35
- # def mock
36
- # @mock || false
37
- # end
38
- end
39
- end