tracking_number 0.10.5 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitmodules +3 -0
- data/.travis.yml +15 -7
- data/Gemfile +2 -0
- data/README.md +87 -15
- data/Rakefile +3 -0
- data/lib/checksum_validations.rb +63 -0
- data/lib/tasks/stats.rake +90 -0
- data/lib/tracking_number.rb +79 -8
- data/lib/tracking_number/base.rb +169 -15
- data/lib/tracking_number/info.rb +18 -0
- data/lib/tracking_number/unknown.rb +33 -0
- data/lib/tracking_number/version.rb +1 -1
- data/test/test_helper.rb +10 -2
- data/test/tracking_number_data_test.rb +161 -0
- data/test/tracking_number_test.rb +105 -0
- data/tracking_number.gemspec +3 -2
- metadata +52 -23
- data/VERSION +0 -1
- data/lib/tracking_number/dhl.rb +0 -34
- data/lib/tracking_number/fedex.rb +0 -156
- data/lib/tracking_number/ontrac.rb +0 -34
- data/lib/tracking_number/ups.rb +0 -108
- data/lib/tracking_number/usps.rb +0 -166
- data/test/dhl_tracking_number_test.rb +0 -35
- data/test/fedex_tracking_number_test.rb +0 -75
- data/test/ontrac_tracking_number_test.rb +0 -24
- data/test/ups_tracking_number_test.rb +0 -36
- data/test/usps_tracking_number_test.rb +0 -73
data/lib/tracking_number/base.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require 'checksum_validations'
|
2
|
+
require 'pry'
|
3
|
+
require 'active_support'
|
4
|
+
|
1
5
|
module TrackingNumber
|
2
6
|
class Base
|
3
7
|
attr_accessor :tracking_number
|
@@ -20,34 +24,80 @@ module TrackingNumber
|
|
20
24
|
end
|
21
25
|
|
22
26
|
def self.scan(body)
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
# matches with match groups within the match data
|
28
|
+
matches = []
|
29
|
+
|
30
|
+
body.scan(self.const_get(:SEARCH_PATTERN)){
|
31
|
+
#get the match data instead, which is needed with these types of regexes
|
32
|
+
matches << $~
|
33
|
+
}
|
34
|
+
|
35
|
+
if matches
|
36
|
+
matches.collect { |m| m[0] }
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def serial_number
|
43
|
+
return match_group("SerialNumber") unless self.class.const_get("VALIDATION")
|
44
|
+
|
45
|
+
format_info = self.class.const_get(:VALIDATION)[:serial_number_format]
|
46
|
+
raw_serial = match_group("SerialNumber")
|
47
|
+
|
48
|
+
if format_info
|
49
|
+
if format_info[:prepend_if] && raw_serial.match(Regexp.new(format_info[:prepend_if][:matches_regex]))
|
50
|
+
return "#{format_info[:prepend_if][:content]}#{raw_serial}"
|
51
|
+
elsif format_info[:prepend_if_missing]
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return raw_serial
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_digit
|
60
|
+
match_group("CheckDigit")
|
61
|
+
end
|
62
|
+
|
63
|
+
def decode
|
64
|
+
decoded = {}
|
65
|
+
(self.matches.try(:names) || []).each do |name|
|
66
|
+
sym = name.underscore.to_sym
|
67
|
+
decoded[sym] = self.matches[name]
|
26
68
|
end
|
27
69
|
|
28
|
-
|
70
|
+
decoded
|
29
71
|
end
|
30
72
|
|
31
73
|
def valid?
|
32
74
|
return false unless valid_format?
|
33
75
|
return false unless valid_checksum?
|
76
|
+
return false unless valid_optional_checks?
|
34
77
|
return true
|
35
78
|
end
|
36
79
|
|
37
80
|
def valid_format?
|
38
|
-
!matches.nil?
|
81
|
+
!matches.nil?
|
39
82
|
end
|
40
83
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
84
|
+
def valid_optional_checks?
|
85
|
+
additional_check = self.class.const_get("VALIDATION")[:additional]
|
86
|
+
return true unless additional_check
|
44
87
|
|
45
|
-
|
46
|
-
[]
|
88
|
+
exist_checks = (additional_check[:exists] ||= [])
|
89
|
+
exist_checks.all? { |w| matching_additional[w] }
|
47
90
|
end
|
48
91
|
|
49
92
|
def valid_checksum?
|
50
|
-
false
|
93
|
+
return false unless self.valid_format?
|
94
|
+
checksum_info = self.class.const_get(:VALIDATION)[:checksum]
|
95
|
+
return true unless checksum_info
|
96
|
+
|
97
|
+
name = checksum_info[:name]
|
98
|
+
method_name = "validates_#{name}?"
|
99
|
+
|
100
|
+
ChecksumValidations.send(method_name, serial_number, check_digit, checksum_info)
|
51
101
|
end
|
52
102
|
|
53
103
|
def to_s
|
@@ -57,11 +107,115 @@ module TrackingNumber
|
|
57
107
|
def inspect
|
58
108
|
"#<%s:%#0x %s>" % [self.class.to_s, self.object_id, tracking_number]
|
59
109
|
end
|
60
|
-
end
|
61
110
|
|
62
|
-
|
63
|
-
|
64
|
-
|
111
|
+
def info
|
112
|
+
Info.new({
|
113
|
+
:courier => courier_info,
|
114
|
+
:service_type => service_type,
|
115
|
+
:service_description => service_description,
|
116
|
+
:destination_zip => destination_zip,
|
117
|
+
:shipper_id => shipper_id,
|
118
|
+
:package_type => package_type,
|
119
|
+
:package_description => package_description
|
120
|
+
})
|
121
|
+
end
|
122
|
+
|
123
|
+
def courier_code
|
124
|
+
self.class.const_get(:COURIER_CODE).to_sym
|
125
|
+
end
|
126
|
+
|
127
|
+
def courier_name
|
128
|
+
if matching_additional["Courier"]
|
129
|
+
matching_additional["Courier"][:courier]
|
130
|
+
else
|
131
|
+
if self.class.constants.include?(:COURIER_INFO)
|
132
|
+
self.class.const_get(:COURIER_INFO)[:name]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
alias_method :carrier, :courier_code #OG tracking_number gem used :carrier.
|
138
|
+
alias_method :carrier_code, :courier_code
|
139
|
+
alias_method :carrier_name, :courier_name
|
140
|
+
|
141
|
+
def courier_info
|
142
|
+
basics = {:name => courier_name, :code => courier_code}
|
143
|
+
|
144
|
+
if info = matching_additional["Courier"]
|
145
|
+
basics.merge!(:name => info[:courier], :url => info[:courier_url], :country => info[:country])
|
146
|
+
end
|
147
|
+
|
148
|
+
@courier ||= Info.new(basics)
|
149
|
+
end
|
150
|
+
|
151
|
+
def service_type
|
152
|
+
if matching_additional["Service Type"]
|
153
|
+
@service_type ||= Info.new(matching_additional["Service Type"]).name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def service_description
|
158
|
+
if matching_additional["Service Type"]
|
159
|
+
@service_description ||= Info.new(matching_additional["Service Type"]).description
|
160
|
+
end
|
65
161
|
end
|
162
|
+
|
163
|
+
def package_type
|
164
|
+
if matching_additional["ContainerType"]
|
165
|
+
@package_type ||= Info.new(matching_additional["Container Type"]).package_info.name
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def destination_zip
|
170
|
+
match_group("DestinationZip")
|
171
|
+
end
|
172
|
+
|
173
|
+
def shipper_id
|
174
|
+
match_group("ShipperId")
|
175
|
+
end
|
176
|
+
|
177
|
+
def matching_additional
|
178
|
+
additional = self.class.const_get(:ADDITIONAL) || []
|
179
|
+
|
180
|
+
relevant_sections = {}
|
181
|
+
|
182
|
+
additional.each do |info|
|
183
|
+
if self.matches && self.matches.length > 0
|
184
|
+
if value = self.matches[info[:regex_group_name]].gsub(/\s/, "")
|
185
|
+
# has matching value
|
186
|
+
matches = info[:lookup].find do |i|
|
187
|
+
if i[:matches]
|
188
|
+
value == i[:matches]
|
189
|
+
elsif i[:matches_regex]
|
190
|
+
value =~ Regexp.new(i[:matches_regex])
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
relevant_sections[info[:name]] = matches
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
relevant_sections
|
200
|
+
end
|
201
|
+
|
202
|
+
protected
|
203
|
+
|
204
|
+
def matches
|
205
|
+
if self.class.constants.include?(:VERIFY_PATTERN)
|
206
|
+
self.tracking_number.match(self.class.const_get(:VERIFY_PATTERN))
|
207
|
+
else
|
208
|
+
[]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def match_group(name)
|
213
|
+
begin
|
214
|
+
self.matches[name].gsub(/\s/, '')
|
215
|
+
rescue
|
216
|
+
nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
66
220
|
end
|
67
221
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TrackingNumber
|
2
|
+
class Info
|
3
|
+
def initialize(info_hash = {})
|
4
|
+
info_hash.keys.each do |key|
|
5
|
+
self.instance_variable_set("@#{key}", info_hash[key])
|
6
|
+
self.class_eval { attr_accessor key }
|
7
|
+
end
|
8
|
+
|
9
|
+
if info_hash.keys.size == 1
|
10
|
+
@default = info_hash[info_hash.keys.first]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@default || @name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module TrackingNumber
|
2
|
+
class Unknown < Base
|
3
|
+
COURIER_CODE = :unknown
|
4
|
+
|
5
|
+
def carrier
|
6
|
+
COURIER_CODE
|
7
|
+
end
|
8
|
+
|
9
|
+
def courier_name
|
10
|
+
"Unknown"
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid_format?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid_checksum?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def decode
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
|
29
|
+
def matching_additional
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -2,6 +2,7 @@ require 'simplecov'
|
|
2
2
|
SimpleCov.start
|
3
3
|
require 'rubygems'
|
4
4
|
require 'minitest/autorun'
|
5
|
+
require 'minitest/reporters'
|
5
6
|
require 'shoulda'
|
6
7
|
require 'active_model'
|
7
8
|
|
@@ -9,6 +10,8 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
9
10
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
10
11
|
require 'tracking_number'
|
11
12
|
|
13
|
+
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(:color => true)]
|
14
|
+
|
12
15
|
class Minitest::Test
|
13
16
|
def possible_numbers(tracking)
|
14
17
|
tracking = tracking.to_s
|
@@ -33,7 +36,7 @@ class Minitest::Test
|
|
33
36
|
def should_detect_number_variants(valid_number, type)
|
34
37
|
possible_strings(valid_number).each do |string|
|
35
38
|
results = type.search(string)
|
36
|
-
assert_equal 1, results.size, "could not find #{type} #{valid_number} in #{string}"
|
39
|
+
assert_equal 1, results.size, "could not find #{type} #{valid_number} in #{string} using search regex: #{type::SEARCH_PATTERN}"
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
@@ -44,8 +47,13 @@ class Minitest::Test
|
|
44
47
|
assert t.valid?
|
45
48
|
end
|
46
49
|
|
50
|
+
def should_be_invalid_number(invalid_number, type, carrier)
|
51
|
+
t = TrackingNumber.new(invalid_number)
|
52
|
+
assert !t.valid?
|
53
|
+
end
|
54
|
+
|
47
55
|
def should_fail_on_check_digit_changes(valid_number)
|
48
|
-
digits = valid_number.chars.to_a
|
56
|
+
digits = valid_number.gsub(/\s/, "").chars.to_a
|
49
57
|
last = digits.pop.to_i
|
50
58
|
digits << (last < 2 ? last + 3 : last - 3).to_s
|
51
59
|
invalid_number = digits.join
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
def load_courier_data(name = :all)
|
4
|
+
if name == :all
|
5
|
+
Dir.glob(File.join(File.dirname(__FILE__), "../lib/data/couriers/*.json")).collect do |file|
|
6
|
+
JSON.parse(File.read(file)).deep_symbolize_keys!
|
7
|
+
end
|
8
|
+
else
|
9
|
+
return JSON.parse(File.join(File.dirname(__FILE__), "../lib/data/couriers/#{name}.json"))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class TrackingNumberDataTest < Minitest::Test
|
14
|
+
load_courier_data(:all).each do |courier_info|
|
15
|
+
courier_name = courier_info[:name]
|
16
|
+
|
17
|
+
courier_code = courier_info[:courier_code].to_sym
|
18
|
+
|
19
|
+
courier_info[:tracking_numbers].each do |tracking_info|
|
20
|
+
klass_name = tracking_info[:name].gsub(/[^0-9A-Za-z]/, '')
|
21
|
+
klass = "TrackingNumber::#{klass_name}".constantize
|
22
|
+
context "[#{tracking_info[:name]}]" do
|
23
|
+
|
24
|
+
single_valid_number = tracking_info[:test_numbers][:valid].first
|
25
|
+
|
26
|
+
tracking_info[:test_numbers][:valid].each do |valid_number|
|
27
|
+
should "validate #{valid_number} with #{klass_name}" do
|
28
|
+
t = klass.new(valid_number)
|
29
|
+
assert_equal courier_code, t.carrier
|
30
|
+
assert t.valid?, "should be valid"
|
31
|
+
end
|
32
|
+
|
33
|
+
should "detect #{valid_number} as #{klass_name}" do
|
34
|
+
#TODO fix this multiple matching thing
|
35
|
+
matches = TrackingNumber.search(valid_number)
|
36
|
+
assert matches.collect(&:class).include?(klass)
|
37
|
+
end
|
38
|
+
|
39
|
+
if tracking_info[:validation][:checksum]
|
40
|
+
# only run this test if number format has checksum
|
41
|
+
should "fail on check digit changes with #{valid_number}" do
|
42
|
+
should_fail_on_check_digit_changes(valid_number)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
should "detect #{valid_number} regardless of spacing" do
|
47
|
+
should_detect_number_variants(valid_number, "TrackingNumber::#{klass_name}".constantize)
|
48
|
+
end
|
49
|
+
|
50
|
+
should "return correct courier code on #{valid_number} when calling #courier_code" do
|
51
|
+
t = klass.new(valid_number)
|
52
|
+
assert_equal courier_info[:courier_code].to_sym, t.courier_code
|
53
|
+
assert_equal courier_info[:courier_code].to_sym, t.courier_code
|
54
|
+
end
|
55
|
+
|
56
|
+
should "return correct courier name on #{valid_number} when calling #courier_name" do
|
57
|
+
t = klass.new(valid_number)
|
58
|
+
|
59
|
+
if (t.matching_additional["Courier"])
|
60
|
+
assert_equal t.matching_additional["Courier"][:courier], t.courier_name
|
61
|
+
else
|
62
|
+
assert_equal courier_name, t.courier_name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
should "not throw an error when calling #service_type on #{valid_number}" do
|
67
|
+
t = klass.new(valid_number)
|
68
|
+
service_type = t.service_type
|
69
|
+
assert service_type.is_a?(String) || service_type.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
should "not throw an error when calling #destination on #{valid_number}" do
|
73
|
+
t = klass.new(valid_number)
|
74
|
+
assert t.destination_zip.is_a?(String) || t.destination_zip.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
should "not throw an error when calling #shipper on #{valid_number}" do
|
78
|
+
t = klass.new(valid_number)
|
79
|
+
assert t.shipper_id.is_a?(String) || t.shipper_id.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
should "not throw an error when calling #package_type on #{valid_number}" do
|
83
|
+
t = klass.new(valid_number)
|
84
|
+
assert t.package_type.is_a?(String) || t.package_type.nil?
|
85
|
+
end
|
86
|
+
#
|
87
|
+
# should "not throw an error when calling #info on #{valid_number}" do
|
88
|
+
# t = klass.new(valid_number)
|
89
|
+
# info = t.info
|
90
|
+
# assert info.is_a?(String)
|
91
|
+
# end
|
92
|
+
|
93
|
+
should "not throw an error when calling #decode on #{valid_number}" do
|
94
|
+
t = klass.new(valid_number)
|
95
|
+
decode = t.decode
|
96
|
+
assert decode.is_a?(Hash)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
tracking_info[:test_numbers][:invalid].each do |invalid_number|
|
102
|
+
should "not validate #{invalid_number} with #{klass_name}" do
|
103
|
+
t = klass.new(invalid_number)
|
104
|
+
assert !t.valid?, "should not be valid"
|
105
|
+
end
|
106
|
+
|
107
|
+
should "not throw an error when calling #service_type on invalid number #{invalid_number}" do
|
108
|
+
t = klass.new(invalid_number)
|
109
|
+
service_type = t.service_type
|
110
|
+
assert service_type.is_a?(String) || service_type.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
should "not throw an error when calling #destination_zip on invalid number #{invalid_number}" do
|
114
|
+
t = klass.new(invalid_number)
|
115
|
+
destination = t.destination_zip
|
116
|
+
assert destination.is_a?(String) || destination.nil?
|
117
|
+
end
|
118
|
+
|
119
|
+
should "not throw an error when calling #shipper_id on invalid number #{invalid_number}" do
|
120
|
+
t = klass.new(invalid_number)
|
121
|
+
shipper = t.shipper_id
|
122
|
+
assert shipper.is_a?(String) || shipper.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
should "not throw an error when calling #package_type on invalid number #{invalid_number}" do
|
126
|
+
t = klass.new(invalid_number)
|
127
|
+
assert t.package_type.is_a?(String) || t.package_type.nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
# should "not throw an error when calling #info on invalid number #{invalid_number}" do
|
131
|
+
# t = klass.new(invalid_number)
|
132
|
+
# info = t.info
|
133
|
+
# assert info.is_a?(String)
|
134
|
+
# end
|
135
|
+
|
136
|
+
should "not throw an error when calling #decode on invalid number #{invalid_number}" do
|
137
|
+
t = klass.new(invalid_number)
|
138
|
+
decode = t.decode
|
139
|
+
assert decode.is_a?(Hash)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
test_numbers = []
|
147
|
+
load_courier_data(:all).each do |courier_info|
|
148
|
+
courier_info[:tracking_numbers].each do |tracking_info|
|
149
|
+
test_numbers << tracking_info[:test_numbers][:valid]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
test_numbers.flatten!
|
154
|
+
|
155
|
+
test_numbers.each do |number|
|
156
|
+
matches = TrackingNumber.detect_all(number)
|
157
|
+
if (matches.size > 1)
|
158
|
+
puts "WARNING: #{number.gsub(/\s/, '')} matched multiple types => #{matches.collect { |m| m.class.to_s.split("::").last}}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|