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 +4 -4
- data/CHANGELOG.adoc +3 -0
- data/README.adoc +91 -0
- data/lib/vin-validator.rb +3 -0
- data/lib/vin_validator/api.rb +123 -0
- data/lib/vin_validator/knowledge.rb +185 -0
- data/lib/vin_validator/maker.rb +32 -0
- data/lib/vin_validator/result.rb +19 -0
- data/lib/vin_validator/version.rb +1 -1
- data/lib/vin_validator/wmi.rb +42 -0
- data/makers.json +1830 -0
- data/wmis.json +6062 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38198acef62a76065fc57b5365744bb09b3c85561a7d2b6cbc61ad121caa4585
|
4
|
+
data.tar.gz: 241fedf88d2f32c952bb86ca5d8052daac1838406cd00aa39bd6a919a5a6eb17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12fb8d34b816c5c1d75462ded857db58cc54bb3de8c544e755968c07666f2f78717209d4212aa731d5cf8cd7e6b6dc4333d8929f3b1675abaafca17ba4525d2a
|
7
|
+
data.tar.gz: 7e99a3ba78f2e971c653aa4171465cd1de6491be6b1d369a8ca4d77e873d62f10dfde1533f42428a4f0551d64325683a6946544fb822b4c6cccc69bd8937e37e
|
data/CHANGELOG.adoc
ADDED
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
@@ -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
|
@@ -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
|