telephone_number 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ module TelephoneNumber
2
+ module ClassMethods
3
+ def parse(number, country)
4
+ TelephoneNumber::Number.new(number, country)
5
+ end
6
+
7
+ def valid?(number, country, keys = [])
8
+ parse(number, country).valid?(keys)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,75 @@
1
+ module TelephoneNumber
2
+ require 'nokogiri'
3
+ class DataImporter
4
+ attr_reader :data, :file
5
+
6
+ def initialize(file_name)
7
+ @data = {}
8
+ @file = File.open(file_name){ |f| Nokogiri::XML(f) }
9
+ end
10
+
11
+ def import!(override: false)
12
+ parse_main_data
13
+ save_data_file(override: override)
14
+ end
15
+
16
+ def parse_main_data
17
+ file.css("territories territory").each do |territory|
18
+ country_code = territory.attributes["id"].value.to_sym
19
+ @data[country_code] ||= {}
20
+
21
+ load_base_attributes(@data[country_code], territory)
22
+ load_references(@data[country_code], territory)
23
+ load_validations(@data[country_code], territory)
24
+ load_formats(@data[country_code], territory)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def load_formats(country_data, territory)
31
+ country_data[TelephoneNumber::PhoneData::FORMATS] = territory.css("availableFormats numberFormat").map do |format|
32
+ format_hash = {}.tap do |fhash|
33
+ format.attributes.values.each { |attr| fhash[attr.name.to_sym] = attr.value.delete("\n ") }
34
+ format.elements.each { |child| fhash[child.name.to_sym] = child.text.delete("\n ") }
35
+ end
36
+ end
37
+ end
38
+
39
+ def load_validations(country_data, territory)
40
+ country_data[TelephoneNumber::PhoneData::VALIDATIONS] = {}
41
+ territory.elements.each do |element|
42
+ next if element.name == "references" || element.name == "availableFormats"
43
+ country_data[TelephoneNumber::PhoneData::VALIDATIONS][underscore(element.name).to_sym] = {}.tap do |validation_hash|
44
+ element.elements.each{|child| validation_hash[underscore(child.name).to_sym] = child.text.delete("\n ")}
45
+ end
46
+ end
47
+ end
48
+
49
+ def load_base_attributes(country_data, territory)
50
+ territory.attributes.each do |key, value_object|
51
+ country_data[underscore(key).to_sym] = value_object.value
52
+ end
53
+ end
54
+
55
+ def load_references(country_data, territory)
56
+ country_data[:references] = territory.css("references sourceUrl").map(&:text)
57
+ end
58
+
59
+ def underscore(camel_cased_word)
60
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
61
+ word = camel_cased_word.to_s.gsub(/::/, '/')
62
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
63
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
64
+ word.tr!("-", "_")
65
+ word.downcase!
66
+ word
67
+ end
68
+
69
+ def save_data_file(override: false)
70
+ data_file = override ? "telephone_number_data_override_file.dat" : "#{File.dirname(__FILE__)}/../../data/telephone_number_data_file.dat"
71
+ File.open(data_file, 'wb+') { |f| Marshal.dump(@data, f) }
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,27 @@
1
+ module TelephoneNumber
2
+ class Number
3
+ include TelephoneNumber::Parser
4
+
5
+ attr_reader :original_number, :country, :e164_number, :national_number
6
+
7
+ def initialize(number, country)
8
+ return unless number && country
9
+
10
+ @original_number = sanitize(number)
11
+ @country = country.upcase.to_sym
12
+ @national_number, @e164_number = extract_number_types(@original_number, @country)
13
+ end
14
+
15
+ def valid_types
16
+ @valid_types ||= validate(e164_number, country)
17
+ end
18
+
19
+ def valid?(keys = [])
20
+ keys.empty? ? !valid_types.empty? : !(valid_types & keys.map(&:to_sym)).empty?
21
+ end
22
+
23
+ def format
24
+ @format ||= extract_format(national_number, country)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ module TelephoneNumber
2
+ module Parser
3
+ KEYS_TO_SKIP = [TelephoneNumber::PhoneData::GENERAL,
4
+ TelephoneNumber::PhoneData::AREA_CODE_OPTIONAL]
5
+
6
+ def sanitize(input_number)
7
+ return input_number.gsub(/[^0-9]/, "")
8
+ end
9
+
10
+ def extract_number_types(input_number, country)
11
+ country_data = TelephoneNumber::PhoneData.phone_data[country.to_sym]
12
+
13
+ return [input_number, nil] unless country_data
14
+ country_code = country_data[TelephoneNumber::PhoneData::COUNTRY_CODE]
15
+
16
+ reg_string = "^(#{country_code})?"
17
+ reg_string += "(#{country_data[TelephoneNumber::PhoneData::NATIONAL_PREFIX]})?"
18
+ reg_string += "(#{country_data[TelephoneNumber::PhoneData::VALIDATIONS]\
19
+ [TelephoneNumber::PhoneData::GENERAL]\
20
+ [TelephoneNumber::PhoneData::VALID_PATTERN]})$"
21
+
22
+ match_result = input_number.match(Regexp.new(reg_string)) || []
23
+
24
+ prefix_results = [match_result[1], match_result[2]]
25
+ without_prefix = input_number.sub(prefix_results.join, "")
26
+ [without_prefix, "#{country_code}#{without_prefix}"]
27
+ end
28
+
29
+ def validate(normalized_number, country)
30
+ country_data = TelephoneNumber::PhoneData.phone_data[country.to_sym]
31
+ return [] unless country_data
32
+ applicable_keys = country_data[TelephoneNumber::PhoneData::VALIDATIONS].reject{ |key, _value| KEYS_TO_SKIP.include?(key) }
33
+ applicable_keys.map do |phone_type, validations|
34
+ full = "^(#{country_data[TelephoneNumber::PhoneData::COUNTRY_CODE]})(#{validations[TelephoneNumber::PhoneData::VALID_PATTERN]})$"
35
+ phone_type if normalized_number =~ Regexp.new(full)
36
+ end.compact
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module TelephoneNumber
2
+ module PhoneData
3
+ VALIDATIONS = :validations.freeze
4
+ FORMATS = :formats.freeze
5
+ GENERAL = :general_desc.freeze
6
+ PREMIUM_RATE = :premium_rate.freeze
7
+ TOLL_FREE = :toll_free.freeze
8
+ SHARED_COST = :shared_cost.freeze
9
+ VOIP = :voip.freeze
10
+ PERSONAL_NUMBER = :personal_number.freeze
11
+ UAN = :uan.freeze
12
+ VOICEMAIL = :voicemail.freeze
13
+ FIXED_LINE = :fixed_line.freeze
14
+ MOBILE = :mobile.freeze
15
+ NO_INTERNATIONAL_DIALING = :no_international_dialling.freeze
16
+ AREA_CODE_OPTIONAL = :area_code_optional.freeze
17
+ VALID_PATTERN = :national_number_pattern.freeze
18
+ POSSIBLE_PATTERN = :possible_number_pattern.freeze
19
+ NATIONAL_PREFIX = :national_prefix.freeze
20
+ COUNTRY_CODE = :country_code.freeze
21
+ LEADING_DIGITS = :leading_digits.freeze
22
+ INTERNATIONAL_PREFIX = :international_prefix.freeze
23
+
24
+ def self.phone_data
25
+ @@phone_data ||= load_data
26
+ end
27
+
28
+ def self.load_data
29
+ data_file = "#{File.dirname(__FILE__)}/../../data/telephone_number_data_file.dat"
30
+ main_data = Marshal.load(File.binread(data_file))
31
+ override_data = {}
32
+ override_data = Marshal.load(File.binread(TelephoneNumber.override_file)) if TelephoneNumber.override_file
33
+ return main_data.merge!(override_data)
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,3 @@
1
+ module TelephoneNumber
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ require "telephone_number/version"
2
+
3
+ module TelephoneNumber
4
+ autoload :DataImporter, 'telephone_number/data_importer'
5
+ autoload :Parser, 'telephone_number/parser'
6
+ autoload :Number, 'telephone_number/number'
7
+ autoload :PhoneData, 'telephone_number/phone_data'
8
+ autoload :ClassMethods, 'telephone_number/class_methods'
9
+
10
+ extend ClassMethods
11
+
12
+ # allows users to override the default data
13
+ @@override_file = nil
14
+
15
+ def self.override_file=(file_name)
16
+ @@override_file = file_name
17
+ end
18
+
19
+ def self.override_file
20
+ @@override_file
21
+ end
22
+
23
+ # generates binary file from xml that user gives us
24
+ def self.generate_override_file(file)
25
+ DataImporter.new(file).import!(override: true)
26
+ end
27
+
28
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'telephone_number/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "telephone_number"
8
+ spec.version = TelephoneNumber::VERSION
9
+ spec.author = 'MOBI Wireless Management'
10
+ spec.email = ["adam.fernung@mobiwm.com", "josh.wetzel@mobiwm.com"]
11
+ spec.summary = 'Phone number validation'
12
+ spec.homepage = "https://github.com/mobi/telephone_number"
13
+ spec.license = "MIT"
14
+
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "minitest"
26
+ spec.add_development_dependency "pry"
27
+ spec.add_development_dependency "pry-byebug"
28
+ spec.add_development_dependency "nokogiri"
29
+ spec.add_development_dependency "minitest-focus"
30
+ end