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 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