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