usps-imis-api 0.2.0 → 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
@@ -29,5 +29,9 @@ describe Imis::Api do
29
29
  it 'sends an update from update' do
30
30
  expect(api.with(31092) { update(mm: 15) }.first).to be_a(Hash)
31
31
  end
32
+
33
+ it 'uses a panel correctly' do
34
+ expect(api.with(6374) { panels.vsc.get(1433) }).to be_a(Hash)
35
+ end
32
36
  end
33
37
  end
@@ -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.0'
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.0
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,192 +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
-
9
- attr_reader :token, :token_expiration, :imis_id
10
-
11
- def initialize(skip_authentication: false)
12
- authenticate unless skip_authentication
13
- end
14
-
15
- # Manually set the current ID, if you already have it for a given member
16
- #
17
- def imis_id=(id)
18
- @imis_id = id.to_s
19
- end
20
-
21
- # Convert a member's certificate number into an iMIS ID number
22
- #
23
- def imis_id_for(certificate)
24
- result = query(Imis.configuration.imis_id_query_name, { certificate: certificate })
25
- @imis_id = result['Items']['$values'][0]['ID']
26
- end
27
-
28
- # Run requests as DSL, with specific iMIS ID only maintained for this scope
29
- #
30
- # This should be used with methods that do not change the value of `imis_id`
31
- #
32
- def with(id, &block)
33
- old_id = imis_id
34
- self.imis_id = id
35
- instance_eval(&block)
36
- ensure
37
- self.imis_id = old_id
38
- end
39
-
40
- # Get a business object for the current member
41
- #
42
- def get(business_object_name, url_id: nil)
43
- uri = uri_for(business_object_name, url_id: url_id)
44
- request = Net::HTTP::Get.new(uri)
45
- result = submit(uri, authorize(request))
46
- JSON.parse(result.body)
47
- end
48
-
49
- # Update only specific fields on a business object for the current member
50
- #
51
- # fields - hash of shape: { field_name => new_value }
52
- #
53
- def put_fields(business_object_name, fields, url_id: nil)
54
- updated = filter_fields(business_object_name, fields)
55
- put(business_object_name, updated, url_id: url_id)
56
- end
57
-
58
- # Update a business object for the current member
59
- #
60
- def put(business_object_name, body, url_id: nil)
61
- uri = uri_for(business_object_name, url_id: url_id)
62
- request = Net::HTTP::Put.new(uri)
63
- request.body = JSON.dump(body)
64
- result = submit(uri, authorize(request))
65
- JSON.parse(result.body)
66
- end
67
-
68
- # Create a business object for the current member
69
- #
70
- def post(business_object_name, body, url_id: nil)
71
- uri = uri_for(business_object_name, url_id: url_id)
72
- request = Net::HTTP::Post.new(uri)
73
- request.body = JSON.dump(body)
74
- result = submit(uri, authorize(request))
75
- JSON.parse(result.body)
76
- end
77
-
78
- # Remove a business object for the current member
79
- #
80
- # Returns empty string on success
81
- #
82
- def delete(business_object_name, url_id: nil)
83
- uri = uri_for(business_object_name, url_id: url_id)
84
- request = Net::HTTP::Delete.new(uri)
85
- result = submit(uri, authorize(request))
86
- result.body
87
- end
88
-
89
- # Run an IQA Query
90
- #
91
- # query_name - the full path of the query in IQA, e.g. `$/_ABC/Fiander/iMIS_ID`
92
- # query_params - hash of shape: { param_name => param_value }
93
- #
94
- def query(query_name, query_params = {})
95
- query_params[:QueryName] = query_name
96
- path = "#{QUERY_PATH}?#{query_params.to_query}"
97
- uri = URI(File.join(imis_hostname, path))
98
- request = Net::HTTP::Get.new(uri)
99
- result = submit(uri, authorize(request))
100
- JSON.parse(result.body)
101
- end
102
-
103
- def mapper
104
- @mapper ||= Mapper.new(self)
105
- end
106
-
107
- def update(data)
108
- mapper.update(data)
109
- end
110
-
111
- private
112
-
113
- def client(uri)
114
- Net::HTTP.new(uri.host, uri.port).tap do |http|
115
- http.use_ssl = true
116
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
117
- end
118
- end
119
-
120
- def imis_hostname
121
- Imis.configuration.hostname
122
- end
123
-
124
- # Authorize a request prior to submitting
125
- #
126
- # If the current token is missing/expired, request a new one
127
- #
128
- def authorize(request)
129
- authenticate if token_expiration < Time.now
130
- request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
131
- end
132
-
133
- # Construct a business object API endpoint address
134
- #
135
- def uri_for(business_object_name, url_id: nil)
136
- url_id ||= imis_id
137
- url_id = CGI.escape(url_id)
138
- URI(File.join(imis_hostname, "#{API_PATH}/#{business_object_name}/#{url_id}"))
139
- end
140
-
141
- def submit(uri, request)
142
- client(uri).request(request).tap do |result|
143
- raise Error::Api.from(result) unless result.is_a?(Net::HTTPSuccess)
144
- end
145
- end
146
-
147
- # Authenticate to the iMIS API, and store the access token and expiration time
148
- #
149
- def authenticate
150
- uri = URI(File.join(imis_hostname, AUTHENTICATION_PATH))
151
- req = Net::HTTP::Post.new(uri)
152
- authentication_data = {
153
- grant_type: 'password',
154
- username: Imis.configuration.username,
155
- password: Imis.configuration.password
156
- }
157
- req.body = URI.encode_www_form(authentication_data)
158
- result = submit(uri, req)
159
- json = JSON.parse(result.body)
160
-
161
- @token = json['access_token']
162
- @token_expiration = Time.parse(json['.expires'])
163
- end
164
-
165
- # Manually assemble the matching data structure, with fields in the correct order
166
- #
167
- def filter_fields(business_object_name, fields)
168
- existing = get(business_object_name)
169
-
170
- JSON.parse(JSON.dump(existing)).tap do |updated|
171
- # The first property is always the iMIS ID again
172
- updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
173
-
174
- # Iterate through all existing fields
175
- existing['Properties']['$values'].each do |value|
176
- next unless fields.keys.include?(value['Name'])
177
-
178
- # Strings are not wrapped in the type definition structure
179
- new_value = fields[value['Name']]
180
- if new_value.is_a?(String)
181
- value['Value'] = new_value
182
- else
183
- value['Value']['$value'] = new_value
184
- end
185
-
186
- # Add the completed field with the updated value
187
- updated['Properties']['$values'] << value
188
- end
189
- end
190
- end
191
- end
192
- 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