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.
@@ -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
- patterns = [self.const_get("SEARCH_PATTERN")].flatten
24
- possibles = patterns.collect do |pattern|
25
- body.scan(pattern).uniq.flatten
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
- possibles.flatten.compact.uniq
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? && !matches.empty?
81
+ !matches.nil?
39
82
  end
40
83
 
41
- def decode
42
- {}
43
- end
84
+ def valid_optional_checks?
85
+ additional_check = self.class.const_get("VALIDATION")[:additional]
86
+ return true unless additional_check
44
87
 
45
- def matches
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
- class Unknown < Base
63
- def carrier
64
- :unknown
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
@@ -1,3 +1,3 @@
1
1
  module TrackingNumber
2
- VERSION = "0.10.5"
2
+ VERSION = "1.0.0.pre1"
3
3
  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