telephone_number 0.0.0 → 0.1.0

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