vin-validator 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|