tracking_number 0.10.5 → 1.0.0.pre1
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 +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
|