vin-validator 0.0.1 → 0.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a5df4ad9817a944b9e892d008fea1d0a8100ca175ce877c7bd5b5ea460aedf9
4
- data.tar.gz: 12d2a78d042231773507ae62bb9dbf5937f55b1d927a1d7e5ed687c886c9bffb
3
+ metadata.gz: 38198acef62a76065fc57b5365744bb09b3c85561a7d2b6cbc61ad121caa4585
4
+ data.tar.gz: 241fedf88d2f32c952bb86ca5d8052daac1838406cd00aa39bd6a919a5a6eb17
5
5
  SHA512:
6
- metadata.gz: 0a2fd2840b2e28a9e977af637e98b0e117839994625637824fa0bb810d1d5cd248bdad4c9b5e8f9582fd466e32bd5fef58c260f396b6064a42e30632bcd09dd6
7
- data.tar.gz: e87b6a2d4d135d8db7d3060f0b406b0a2798a90d730270f1c7eb3efc8938656b2d06f5f91cdc8f70baed3ffed492d7988c74624f43560ca8b9993897c8d8cb09
6
+ metadata.gz: 12fb8d34b816c5c1d75462ded857db58cc54bb3de8c544e755968c07666f2f78717209d4212aa731d5cf8cd7e6b6dc4333d8929f3b1675abaafca17ba4525d2a
7
+ data.tar.gz: 7e99a3ba78f2e971c653aa4171465cd1de6491be6b1d369a8ca4d77e873d62f10dfde1533f42428a4f0551d64325683a6946544fb822b4c6cccc69bd8937e37e
data/CHANGELOG.adoc ADDED
@@ -0,0 +1,3 @@
1
+ = 1.0.0
2
+
3
+ * Intial Release
data/README.adoc ADDED
@@ -0,0 +1,91 @@
1
+ = Vin-Validator
2
+
3
+ Gem for gathering info on VINs, optionally using link:https://vpic.nhtsa.dot.gov/api/[NHTSA's API]
4
+
5
+ == Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ [source,ruby]
10
+ ----
11
+ gem 'vin-validator', require: false
12
+ ----
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install vin-validator
21
+
22
+ == Usage
23
+
24
+ [source,ruby]
25
+ ----
26
+ just_a_vin = 'AA5325L1588827'
27
+ other_vins = %w[13N153201K1533942 1E9AA5325L1588827 1RND48A27GR039983 1RND53A33KR049282]
28
+
29
+ VinValidator::Knowledge.vin_info(just_a_vin).fetch(just_a_vin)
30
+
31
+ VinValidator::Knowledge.vin_info(just_a_vin, true).fetch(just_a_vin)
32
+
33
+ VinValidator::Knowledge.vin_info(other_vins)
34
+
35
+ VinValidator::Knowledge.vin_info(other_vins, true)
36
+ ----
37
+
38
+ Every call to ``VinValidator::Knowledge::vin_info`` will return a hash.
39
+ Each key in the result is one of the VINs provided, and each value is another hash.
40
+
41
+ Value hash:
42
+
43
+ |===
44
+ |Key |Value
45
+
46
+ |``:errors``
47
+ |``Array<VinValidator::Result>``
48
+
49
+ |``:nhtsa_errors``
50
+ |``Array<VinValidator::Result>``
51
+
52
+ |``:vin``
53
+ |``VinValidator::Result``
54
+
55
+ |``:make``
56
+ |``Array<VinValidator::Result>``
57
+
58
+ |``:manufacturer``
59
+ |``VinValidator::Result``
60
+
61
+ |``:model``
62
+ |``VinValidator::Result``
63
+
64
+ |``:year``
65
+ |``VinValidator::Result``
66
+
67
+ |``:body_type``
68
+ |``VinValidator::Result``
69
+
70
+ |``:trailer_type``
71
+ |``VinValidator::Result``
72
+
73
+ |``:vehicle_type``
74
+ |``VinValidator::Result``
75
+
76
+ |``:gvw``
77
+ |``VinValidator::Result``
78
+
79
+ |``:suggested_vin``
80
+ |``VinValidator::Result``
81
+ |===
82
+
83
+ ``VinValidator::Result`` has 3 attributes: ``vin``, ``type``, and ``value``.
84
+
85
+ * ``vin`` - The vin the result belongs to
86
+ * ``type`` - The type of result (``error``, ``make``, ``year``, etc)
87
+ * ``value`` - The value of the result
88
+
89
+ == Changelog
90
+
91
+ See xref:./CHANGELOG.adoc[CHANGELOG] see changes
data/lib/vin-validator.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require_relative 'vin_validator/version'
5
+ require_relative 'vin_validator/knowledge'
6
+ require 'net/http'
4
7
 
5
8
  module VinValidator
6
9
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VinValidator
4
+ class Api
5
+ class << self
6
+ # Fetches info about vins. GET request if single VIN is passed in.
7
+ # More details can be found at +https://vpic.nhtsa.dot.gov/api/Home+
8
+ #
9
+ # @param vins [Array<String>, String] vins to check
10
+ #
11
+ # @return [Hash]
12
+ #
13
+ def vin_info(vins)
14
+ check_nhtsa = block_given? ? yield : true
15
+ return {} unless check_nhtsa
16
+
17
+ vins_to_check = [vins].flatten.compact.uniq
18
+
19
+ api_results =
20
+ if vins_to_check.size == 1
21
+ # 1E9AA5325L1588827
22
+ [single_vin_info(vins_to_check.first)]
23
+ else
24
+ # 13N153201K1533942
25
+ # 1E9AA5325L1588827
26
+ # 1RND48A27GR039983
27
+ # 1RND53A33KR049282
28
+ multiple_vin_infos(vins_to_check)
29
+ end
30
+
31
+ results = {}
32
+ api_results.each do |result|
33
+ result[:Results].each do |details|
34
+ results[details[:VIN]] = details
35
+ end
36
+ end
37
+
38
+ # '1R1F24822YK501231' => {
39
+ # ...
40
+ # "TrailerBodyType": "",
41
+ # ...
42
+ # "VIN": "1R1F24822YK501231",
43
+ # ...
44
+ # }
45
+
46
+ return results
47
+ end
48
+
49
+ private
50
+
51
+ # @param vin [String]
52
+ #
53
+ # @return [Hash] info about given vin
54
+ #
55
+ def single_vin_info(vin)
56
+ parse_request(build_get_request(vin))
57
+ end
58
+
59
+ # @param vins [Array<String>]
60
+ #
61
+ # @return [Array<Hash>] info about given vins
62
+ #
63
+ def multiple_vin_infos(vins)
64
+ vins.each_slice(50).map do |vin_list|
65
+ parse_request(build_post_request(vin_list))
66
+ end
67
+ end
68
+
69
+ # @return [Stirng]
70
+ #
71
+ def root_api_url
72
+ 'https://vpic.nhtsa.dot.gov/api/vehicles'
73
+ end
74
+
75
+ # @param vin [String]
76
+ #
77
+ # @return [Hash]
78
+ #
79
+ def build_get_request(vin)
80
+ url = "#{root_api_url}/DecodeVinValues/#{vin}?format=json"
81
+ uri = URI(url)
82
+
83
+ http = Net::HTTP.new(uri.host, uri.port)
84
+ http.use_ssl = true
85
+ # Use 5 instead of 60 (default) seconds for NHTSA
86
+ http.read_timeout = 5
87
+
88
+ request = Net::HTTP::Get.new(uri)
89
+
90
+ return { http: http, request: request }
91
+ end
92
+
93
+ # @param vins [Array<String>]
94
+ #
95
+ # @return [Hash]
96
+ #
97
+ def build_post_request(vins)
98
+ url = "#{root_api_url}/DecodeVINValuesBatch"
99
+ uri = URI(url)
100
+
101
+ http = Net::HTTP.new(uri.host, uri.port)
102
+ http.use_ssl = true
103
+ # Use 5 instead of 60 (default) seconds for NHTSA
104
+ http.read_timeout = 5
105
+
106
+ request = Net::HTTP::Post.new(uri)
107
+ request.set_form_data({ data: vins.join(';'), format: 'JSON' })
108
+
109
+ return { http: http, request: request }
110
+ end
111
+
112
+ # Sends and parses the request
113
+ #
114
+ # @return [Hash<Symbol>]
115
+ #
116
+ def parse_request(http_request)
117
+ response = http_request.fetch(:http).request(http_request.fetch(:request))
118
+
119
+ return JSON.parse(response.body, { symbolize_names: true })
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './result'
4
+ require_relative './maker'
5
+ require_relative './wmi'
6
+ require_relative './api'
7
+
8
+ module VinValidator
9
+ class Knowledge
10
+ class << self
11
+ # Gathers/builds info for each vin provided
12
+ #
13
+ # @param vins [String, Array<String>]
14
+ # @param hit_nhtsa [Boolean] if `true`, query NHTSA. default: `false`
15
+ #
16
+ # @return [Hash]
17
+ #
18
+ def vin_info(vins, hit_nhtsa = false, &block)
19
+ results = hit_nhtsa ? VinValidator::Api.vin_info(vins, &block) : {}
20
+
21
+ Array(vins).to_h do |vin|
22
+ vin_results = results.fetch(vin, {})
23
+ [vin, build_results(vin, vin_results)]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Does the basic checksum and valid character checking
30
+ #
31
+ # @param vin [String, Array<String>] the vin characters to check
32
+ #
33
+ # @return [Array<String>] the errors
34
+ #
35
+ def simple_vin_check(vin)
36
+ return ['Vin is not equal to 17 characters.'] if vin.length != 17
37
+
38
+ vin =
39
+ if vin.is_a?(Array)
40
+ vin.map(&:upcase)
41
+ else
42
+ vin.upcase.split('')
43
+ end
44
+
45
+ vin_errors = []
46
+
47
+ if vin.join('') !~ /^[a-z0-9]{17}$/i
48
+ vin_errors << 'Only numbers and letters are allowed in VINs'
49
+ end
50
+
51
+ # rubocop:disable Style/EachForSimpleLoop
52
+ (0..7).each do |slot|
53
+ # rubocop:enable Style/EachForSimpleLoop
54
+ # For 1-8, can't have 'I', 'O', or 'Q'
55
+ if /[ioq]/i =~ vin[slot].to_s
56
+ vin_errors << "'I', 'O', and 'Q' are not allowed in slot #{slot + 1}."
57
+ end
58
+ end
59
+
60
+ unless /[0-9x]/i =~ vin[8].to_s # For 9, only 0-9 and 'X' are allowed
61
+ vin_errors << "Only 0-9 and 'X' are allowed in slot 9."
62
+ end
63
+
64
+ if /[iquz0]/i =~ vin[9].to_s # For 10, can't have 'I', 'O', 'Q', 'U', 'Z', '0'
65
+ vin_errors << "'I', 'O', 'Q', 'U', 'Z', and 0 are not allowed in slot 10."
66
+ end
67
+
68
+ (10..13).each do |slot|
69
+ # For 11-14, can't have 'I', 'O', 'Q'
70
+ if /[ioq]/i =~ vin[slot].to_s
71
+ vin_errors << "'I', 'O', or 'Q' not allowed in slot #{slot + 1}."
72
+ end
73
+ end
74
+
75
+ # For 15-17, only 0-9
76
+ (14..16).each do |slot|
77
+ unless /[0-9]/i =~ vin[slot].to_s
78
+ vin_errors << "Only 0-9 allowed in slot #{slot + 1}."
79
+ end
80
+ end
81
+
82
+ vin_errors << 'Check digit is incorrect.' unless get_check_digit(vin) == vin[8]
83
+ vin_errors
84
+ end
85
+
86
+ # Calculates the character's value. From wikipedia
87
+ #
88
+ # @return [Number, Nil]
89
+ #
90
+ def transliterate(c)
91
+ '0123456789.ABCDEFGH..JKLMN.P.R..STUVWXYZ'.index(c) % 10
92
+ rescue
93
+ nil
94
+ end
95
+
96
+ # Calculates the expected check digit for the given vin
97
+ #
98
+ # @param vin [String]
99
+ #
100
+ # @return [String]
101
+ #
102
+ def get_check_digit(vin)
103
+ map = '0123456789X'
104
+ weights = '8765432X098765432'
105
+ sum = 0
106
+ (0..16).each do |i|
107
+ value_weight = transliterate(vin[i])
108
+ return nil if value_weight.nil?
109
+
110
+ sum += value_weight * map.index(weights[i])
111
+ end
112
+ return map[sum % 11]
113
+ end
114
+
115
+ # Combines T2 and NHTSA's results. Will attempt to create `VinValidator::Result`s
116
+ #
117
+ # @return [Hash]
118
+ #
119
+ def build_results(vin, vin_nhtsa_results)
120
+ vin_results =
121
+ begin
122
+ parse_nhtsa_results(vin, vin_nhtsa_results)
123
+ rescue
124
+ { errors: [] }
125
+ end
126
+
127
+ simple_errors = simple_vin_check(vin)
128
+ vin_results[:errors] += simple_errors.map do |err|
129
+ VinValidator::Result.new(vin: vin, type: :error, value: err)
130
+ end
131
+
132
+ reset_vin = !vin_results.has_key?(:vin) || vin_results.dig(:vin).nil?
133
+ reset_vin ||= vin_results.dig(:vin).value.nil?
134
+ reset_vin ||= vin_results.dig(:vin).value.empty?
135
+
136
+ if reset_vin
137
+ vin_results[:vin] = VinValidator::Result.new(vin: vin, type: :vin, value: vin)
138
+ end
139
+
140
+ reset_make = !vin_results.has_key?(:make)
141
+ reset_make ||= vin_results.dig(:make, 0).nil?
142
+ reset_make ||= vin_results.dig(:make, 0).value.nil?
143
+ reset_make ||= vin_results.dig(:make, 0).value.empty?
144
+
145
+ if reset_make
146
+ vin_results[:make] =
147
+ VinValidator::Wmi.where(wmi: vin.strip.split('')[0..2].join).map do |wmi|
148
+ VinValidator::Result.new(vin: vin, type: :make, value: wmi.maker.name)
149
+ end
150
+ end
151
+
152
+ return vin_results
153
+ end
154
+
155
+ # @param vin [String] vin to validate
156
+ # @param results [Hash] NHTSA results
157
+ #
158
+ # @return [Hash] errors and addtl info
159
+ #
160
+ def parse_nhtsa_results(vin, results)
161
+ return { errors: [] } if results.empty?
162
+
163
+ errors = []
164
+ errors += results[:ErrorText].split('; ') unless results[:ErrorCode] == '0'
165
+ errors += Array(results[:AdditionalErrorText])
166
+
167
+ return {
168
+ errors: [],
169
+ # `nhtsa_errors` because they find too many errors that shouldn't trigger
170
+ nhtsa_errors: errors.map { |err| VinValidator::Result.new(vin: vin, value: err, type: :error) },
171
+ vin: VinValidator::Result.new(vin: vin, value: results[:VIN], type: :vin),
172
+ make: Array(VinValidator::Result.new(vin: vin, value: results[:Make], type: :make)),
173
+ manufacturer: VinValidator::Result.new(vin: vin, value: results[:Manufacturer], type: :manufacturer),
174
+ model: VinValidator::Result.new(vin: vin, value: results[:Model], type: :model),
175
+ year: VinValidator::Result.new(vin: vin, value: results[:ModelYear], type: :year),
176
+ body_type: VinValidator::Result.new(vin: vin, value: results[:TrailerBodyType], type: :body_type),
177
+ trailer_type: VinValidator::Result.new(vin: vin, value: results[:TrailerType], type: :trailer_type),
178
+ vehicle_type: VinValidator::Result.new(vin: vin, value: results[:VehicleType], type: :vehicle_type),
179
+ gvw: VinValidator::Result.new(vin: vin, value: results[:GVWR], type: :gvw),
180
+ suggested_vin: VinValidator::Result.new(vin: vin, value: results[:SuggestedVIN], type: :suggested_vin)
181
+ }
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VinValidator
4
+ class Maker
5
+ class << self
6
+ def all
7
+ return @all if defined?(@all)
8
+
9
+ @all = JSON.parse(File.read('makers.json'), { symbolize_names: true }).to_h { |id, v| [id.to_s, new(**v)] }
10
+ end
11
+
12
+ def find(id)
13
+ all.fetch(id.to_s)
14
+ end
15
+
16
+ def where(wmi:)
17
+
18
+ end
19
+ end
20
+
21
+ # @return [Integer]
22
+ attr_accessor :id
23
+ # @return [String]
24
+ attr_accessor :name
25
+
26
+ # :nodoc:
27
+ def initialize(id:, name:)
28
+ @id = id
29
+ @name = name
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VinValidator
4
+ class Result
5
+ # @return [String]
6
+ attr_accessor :vin
7
+ # @return [String]
8
+ attr_accessor :type
9
+ # @return [String]
10
+ attr_accessor :value
11
+
12
+ # :nodoc:
13
+ def initialize(vin:, type:, value:)
14
+ @vin = vin
15
+ @type = type
16
+ @value = value
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VinValidator
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.3'
5
5
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VinValidator
4
+ class Wmi
5
+ class << self
6
+ def all
7
+ return @all if defined?(@all)
8
+
9
+ @all = JSON.parse(File.read('wmis.json'), { symbolize_names: true }).to_h { |id, v| [id.to_s, new(**v)] }
10
+ end
11
+
12
+ def find(id)
13
+ all.fetch(id.to_s)
14
+ end
15
+
16
+ def where(wmi:)
17
+ all.values.select { |w| w.wmi == wmi }
18
+ end
19
+ end
20
+
21
+ # @return [Integer]
22
+ attr_accessor :id
23
+ # @return [String]
24
+ attr_accessor :wmi
25
+ # @return [String, Nil]
26
+ attr_accessor :wmi_suffix
27
+ # @return [Integer]
28
+ attr_accessor :maker_id
29
+
30
+ # :nodoc:
31
+ def initialize(id:, wmi:, maker_id:, wmi_suffix: nil)
32
+ @id = id
33
+ @maker_id = maker_id
34
+ @wmi = wmi
35
+ @wmi_suffix = wmi_suffix
36
+ end
37
+
38
+ def maker
39
+ @maker ||= VinValidator::Maker.find(maker_id)
40
+ end
41
+ end
42
+ end