addressing 2.0.1 → 2.2.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +223 -111
  4. data/data/UPSTREAM_VERSION +1 -0
  5. data/data/address_formats.json +2256 -206
  6. data/data/countries.json +1282 -0
  7. data/data/locale.json +222 -0
  8. data/data/subdivision/AE.json +1 -1
  9. data/data/subdivision/BR-AC.json +22 -22
  10. data/data/subdivision/BR-AL.json +102 -102
  11. data/data/subdivision/BR-AM.json +62 -62
  12. data/data/subdivision/BR-AP.json +16 -16
  13. data/data/subdivision/BR-BA.json +417 -417
  14. data/data/subdivision/BR-CE.json +184 -184
  15. data/data/subdivision/BR-DF.json +1 -1
  16. data/data/subdivision/BR-ES.json +79 -79
  17. data/data/subdivision/BR-GO.json +246 -246
  18. data/data/subdivision/BR-MA.json +217 -217
  19. data/data/subdivision/BR-MG.json +853 -853
  20. data/data/subdivision/BR-MS.json +78 -78
  21. data/data/subdivision/BR-MT.json +141 -141
  22. data/data/subdivision/BR-PA.json +144 -144
  23. data/data/subdivision/BR-PB.json +223 -223
  24. data/data/subdivision/BR-PE.json +185 -185
  25. data/data/subdivision/BR-PI.json +223 -223
  26. data/data/subdivision/BR-PR.json +400 -400
  27. data/data/subdivision/BR-RJ.json +93 -93
  28. data/data/subdivision/BR-RN.json +166 -166
  29. data/data/subdivision/BR-RO.json +52 -52
  30. data/data/subdivision/BR-RR.json +15 -15
  31. data/data/subdivision/BR-RS.json +497 -497
  32. data/data/subdivision/BR-SC.json +295 -295
  33. data/data/subdivision/BR-SE.json +75 -75
  34. data/data/subdivision/BR-SP.json +645 -645
  35. data/data/subdivision/BR-TO.json +139 -139
  36. data/data/subdivision/CA.json +6 -12
  37. data/data/subdivision/CL-AI.json +10 -10
  38. data/data/subdivision/CL-AN.json +9 -9
  39. data/data/subdivision/CL-AP.json +4 -4
  40. data/data/subdivision/CL-AR.json +32 -32
  41. data/data/subdivision/CL-AT.json +9 -9
  42. data/data/subdivision/CL-BI.json +34 -34
  43. data/data/subdivision/CL-CO.json +15 -15
  44. data/data/subdivision/CL-LI.json +33 -33
  45. data/data/subdivision/CL-LL.json +30 -30
  46. data/data/subdivision/CL-LR.json +12 -12
  47. data/data/subdivision/CL-MA.json +11 -11
  48. data/data/subdivision/CL-ML.json +30 -30
  49. data/data/subdivision/CL-NB.json +21 -21
  50. data/data/subdivision/CL-RM.json +52 -52
  51. data/data/subdivision/CL-TA.json +7 -7
  52. data/data/subdivision/CL-VS.json +38 -38
  53. data/data/subdivision/CO.json +1 -1
  54. data/data/subdivision/CV.json +9 -9
  55. data/data/subdivision/GB.json +675 -0
  56. data/data/subdivision/GT.json +71 -0
  57. data/data/subdivision/IT.json +13 -1
  58. data/data/subdivision/KY.json +3 -3
  59. data/data/subdivision/NG.json +1 -1
  60. data/data/subdivision/RU.json +4 -4
  61. data/data/subdivision/TH.json +7 -7
  62. data/data/subdivision/TR.json +1 -1
  63. data/data/subdivision/TV.json +1 -1
  64. data/data/subdivision/VE.json +4 -4
  65. data/data/subdivision/VN.json +1 -1
  66. data/lib/addressing/address.rb +7 -3
  67. data/lib/addressing/address_format.rb +93 -97
  68. data/lib/addressing/address_validator.rb +98 -0
  69. data/lib/addressing/blank.rb +32 -0
  70. data/lib/addressing/country.rb +28 -314
  71. data/lib/addressing/data_source.rb +75 -0
  72. data/lib/addressing/default_formatter.rb +61 -33
  73. data/lib/addressing/enum.rb +2 -6
  74. data/lib/addressing/field_violation.rb +17 -0
  75. data/lib/addressing/lazy_subdivisions.rb +16 -15
  76. data/lib/addressing/locale.rb +20 -269
  77. data/lib/addressing/model.rb +20 -79
  78. data/lib/addressing/postal_label_formatter.rb +16 -15
  79. data/lib/addressing/subdivision.rb +121 -106
  80. data/lib/addressing/version.rb +1 -1
  81. data/lib/addressing.rb +6 -1
  82. metadata +11 -3
  83. data/data/address_formats.dump +0 -0
@@ -0,0 +1,71 @@
1
+ {
2
+ "country_code": "GT",
3
+ "subdivisions": {
4
+ "16": {
5
+ "name": "Alta Verapaz"
6
+ },
7
+ "15": {
8
+ "name": "Baja Verapaz"
9
+ },
10
+ "04": {
11
+ "name": "Chimaltenango"
12
+ },
13
+ "20": {
14
+ "name": "Chiquimula"
15
+ },
16
+ "02": {
17
+ "name": "El Progreso"
18
+ },
19
+ "05": {
20
+ "name": "Escuintla"
21
+ },
22
+ "01": {
23
+ "name": "Guatemala"
24
+ },
25
+ "13": {
26
+ "name": "Huehuetenango"
27
+ },
28
+ "18": {
29
+ "name": "Izabal"
30
+ },
31
+ "21": {
32
+ "name": "Jalapa"
33
+ },
34
+ "22": {
35
+ "name": "Jutiapa"
36
+ },
37
+ "17": {
38
+ "name": "Petén"
39
+ },
40
+ "09": {
41
+ "name": "Quetzaltenango"
42
+ },
43
+ "14": {
44
+ "name": "Quiché"
45
+ },
46
+ "11": {
47
+ "name": "Retalhuleu"
48
+ },
49
+ "03": {
50
+ "name": "Sacatepéquez"
51
+ },
52
+ "12": {
53
+ "name": "San Marcos"
54
+ },
55
+ "06": {
56
+ "name": "Santa Rosa"
57
+ },
58
+ "07": {
59
+ "name": "Sololá"
60
+ },
61
+ "10": {
62
+ "name": "Suchitepéquez"
63
+ },
64
+ "08": {
65
+ "name": "Totonicapán"
66
+ },
67
+ "19": {
68
+ "name": "Zacapa"
69
+ }
70
+ }
71
+ }
@@ -149,6 +149,10 @@
149
149
  "code": "FR",
150
150
  "name": "Frosinone"
151
151
  },
152
+ "OT": {
153
+ "code": "OT",
154
+ "name": "Gallura Nord-Est Sardegna"
155
+ },
152
156
  "GE": {
153
157
  "code": "GE",
154
158
  "name": "Genova"
@@ -217,6 +221,10 @@
217
221
  "code": "MT",
218
222
  "name": "Matera"
219
223
  },
224
+ "VS": {
225
+ "code": "VS",
226
+ "name": "Medio Campidano"
227
+ },
220
228
  "ME": {
221
229
  "code": "ME",
222
230
  "name": "Messina"
@@ -245,6 +253,10 @@
245
253
  "code": "NU",
246
254
  "name": "Nuoro"
247
255
  },
256
+ "OG": {
257
+ "code": "OG",
258
+ "name": "Ogliastra"
259
+ },
248
260
  "OR": {
249
261
  "code": "OR",
250
262
  "name": "Oristano"
@@ -359,7 +371,7 @@
359
371
  },
360
372
  "SU": {
361
373
  "code": "SU",
362
- "name": "Sud Sardegna"
374
+ "name": "Sulcis Iglesiente"
363
375
  },
364
376
  "TA": {
365
377
  "code": "TA",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "country_code": "KY",
3
3
  "subdivisions": {
4
- "Cayman Brac": {},
5
- "Grand Cayman": {},
6
- "Little Cayman": {}
4
+ "Cayman Brac": [],
5
+ "Grand Cayman": [],
6
+ "Little Cayman": []
7
7
  }
8
8
  }
@@ -83,7 +83,7 @@
83
83
  "name": "Niger"
84
84
  },
85
85
  "OG": {
86
- "name": "Ogun State"
86
+ "name": "Ogun"
87
87
  },
88
88
  "ON": {
89
89
  "name": "Ondo"
@@ -240,10 +240,6 @@
240
240
  "name": "Tyva Republits",
241
241
  "local_name": "Республика Тыва"
242
242
  },
243
- "UD": {
244
- "name": "Udmurtskaja Respublika",
245
- "local_name": "Республика Удмуртия"
246
- },
247
243
  "KK": {
248
244
  "name": "Khakasiya Republits",
249
245
  "local_name": "Республика Хакасия"
@@ -308,6 +304,10 @@
308
304
  "name": "Tyumenskaya oblast'",
309
305
  "local_name": "Тюменская область"
310
306
  },
307
+ "UD": {
308
+ "name": "Udmurtskaja Respublika",
309
+ "local_name": "Удмуртская Республика"
310
+ },
311
311
  "ULY": {
312
312
  "name": "Ulyanovskaya oblast'",
313
313
  "local_name": "Ульяновская область"
@@ -26,10 +26,6 @@
26
26
  "name": "Khon Kaen",
27
27
  "local_name": "ขอนแก่น"
28
28
  },
29
- "38": {
30
- "name": "Bueng Kan",
31
- "local_name": "จังหวัด บึงกาฬ"
32
- },
33
29
  "22": {
34
30
  "name": "Chanthaburi",
35
31
  "local_name": "จันทบุรี"
@@ -51,7 +47,7 @@
51
47
  "local_name": "ชัยภูมิ"
52
48
  },
53
49
  "86": {
54
- "name": "Chumpon",
50
+ "name": "Chumphon",
55
51
  "local_name": "ชุมพร"
56
52
  },
57
53
  "57": {
@@ -110,6 +106,10 @@
110
106
  "name": "Nan",
111
107
  "local_name": "น่าน"
112
108
  },
109
+ "38": {
110
+ "name": "Bueng Kan",
111
+ "local_name": "บึงกาฬ"
112
+ },
113
113
  "31": {
114
114
  "name": "Buri Ram",
115
115
  "local_name": "บุรีรัมย์"
@@ -143,7 +143,7 @@
143
143
  "local_name": "พังงา"
144
144
  },
145
145
  "93": {
146
- "name": "Phattalung",
146
+ "name": "Phatthalung",
147
147
  "local_name": "พัทลุง"
148
148
  },
149
149
  "66": {
@@ -267,7 +267,7 @@
267
267
  "local_name": "สุโขทัย"
268
268
  },
269
269
  "72": {
270
- "name": "Suphanburi",
270
+ "name": "Suphan Buri",
271
271
  "local_name": "สุพรรณบุรี"
272
272
  },
273
273
  "84": {
@@ -8,7 +8,7 @@
8
8
  "name": "Adıyaman"
9
9
  },
10
10
  "03": {
11
- "name": "Afyon"
11
+ "name": "Afyonkarahisar"
12
12
  },
13
13
  "04": {
14
14
  "name": "Ağrı"
@@ -10,7 +10,7 @@
10
10
  "NMA": {
11
11
  "name": "Nanumea"
12
12
  },
13
- "Niulakita": {},
13
+ "Niulakita": [],
14
14
  "NIT": {
15
15
  "name": "Niutao"
16
16
  },
@@ -32,7 +32,7 @@
32
32
  "name": "Dependencias Federales"
33
33
  },
34
34
  "A": {
35
- "name": "Distrito Federal"
35
+ "name": "Distrito Capital"
36
36
  },
37
37
  "I": {
38
38
  "name": "Falcón"
@@ -40,6 +40,9 @@
40
40
  "J": {
41
41
  "name": "Guárico"
42
42
  },
43
+ "X": {
44
+ "name": "La Guaira"
45
+ },
43
46
  "K": {
44
47
  "name": "Lara"
45
48
  },
@@ -67,9 +70,6 @@
67
70
  "T": {
68
71
  "name": "Trujillo"
69
72
  },
70
- "X": {
71
- "name": "La Guaira"
72
- },
73
73
  "U": {
74
74
  "name": "Yaracuy"
75
75
  },
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "79": {
62
62
  "name": "Ho Chi Minh City",
63
- "local_name": "Thành phố Hồ Chí Minh"
63
+ "local_name": "Hồ Chí Minh"
64
64
  },
65
65
  "46": {
66
66
  "name": "Hue City",
@@ -28,7 +28,7 @@ module Addressing
28
28
 
29
29
  # Creates a new Address instance.
30
30
  #
31
- # @param country_code [String] ISO 3166-1 alpha-2 country code
31
+ # @param country_code [String] ISO 3166-1 alpha-2 country code, stored in uppercase
32
32
  # @param administrative_area [String] Top-level administrative subdivision (state, province, etc.)
33
33
  # @param locality [String] City or locality
34
34
  # @param dependent_locality [String] Dependent locality (neighborhood, suburb, district, etc.)
@@ -43,7 +43,7 @@ module Addressing
43
43
  # @param family_name [String] Family name (last name)
44
44
  # @param locale [String] Locale code for the address
45
45
  def initialize(country_code: "", administrative_area: "", locality: "", dependent_locality: "", postal_code: "", sorting_code: "", address_line1: "", address_line2: "", address_line3: "", organization: "", given_name: "", additional_name: "", family_name: "", locale: "und")
46
- @country_code = country_code
46
+ self.country_code = country_code
47
47
  @administrative_area = administrative_area
48
48
  @locality = locality
49
49
  @dependent_locality = dependent_locality
@@ -95,6 +95,10 @@ module Addressing
95
95
 
96
96
  protected
97
97
 
98
- attr_writer(*FIELDS)
98
+ attr_writer(*(FIELDS - [:country_code]))
99
+
100
+ def country_code=(value)
101
+ @country_code = value.is_a?(String) ? value.upcase : value
102
+ end
99
103
  end
100
104
  end
@@ -8,9 +8,35 @@ module Addressing
8
8
  #
9
9
  # @example Get address format for Brazil
10
10
  # format = Addressing::AddressFormat.get('BR')
11
- # format.used_fields # => ["given_name", "family_name", ...]
12
- # format.required_fields # => ["address_line1", "locality", ...]
11
+ # format.used_fields # => ["given_name", "family_name", ...]
12
+ # format.required_fields # => ["address_line1", "locality", ...]
13
+ # format.subdivision_fields # => ["administrative_area", "locality"]
13
14
  class AddressFormat
15
+ # The subdivision fields, ordered from the top level down.
16
+ SUBDIVISION_FIELDS = [
17
+ AddressField::ADMINISTRATIVE_AREA,
18
+ AddressField::LOCALITY,
19
+ AddressField::DEPENDENT_LOCALITY
20
+ ].freeze
21
+
22
+ # The defaults for every address format, including the fallback for
23
+ # countries without a definition.
24
+ GENERIC_DEFINITION = {
25
+ format: "%given_name %family_name\n%organization\n%address_line1\n%address_line2\n%address_line3\n%locality",
26
+ required_fields: [
27
+ "address_line1", "locality"
28
+ ].freeze,
29
+ uppercase_fields: [
30
+ "locality"
31
+ ].freeze,
32
+ subdivision_fields: [].freeze,
33
+ administrative_area_type: "province",
34
+ locality_type: "city",
35
+ dependent_locality_type: "suburb",
36
+ postal_code_type: "postal"
37
+ }.freeze
38
+ private_constant :GENERIC_DEFINITION
39
+
14
40
  class << self
15
41
  # Gets the address format for the provided country code.
16
42
  #
@@ -18,58 +44,65 @@ module Addressing
18
44
  # @return [AddressFormat] Address format instance
19
45
  def get(country_code)
20
46
  country_code = country_code.upcase
21
- @address_formats ||= {}
22
-
23
- unless @address_formats.key?(country_code)
24
- definition = process_definition(definitions[country_code] || {country_code: country_code})
25
- @address_formats[country_code] = new(definition)
26
- end
27
-
28
- @address_formats[country_code]
47
+ # Unknown country codes often come from user input, so they are not cached.
48
+ address_formats.fetch(country_code) { new(process_definition(country_code: country_code)) }
29
49
  end
30
50
 
31
51
  def all
32
- definitions.map do |country_code, definition|
33
- definition = process_definition(definition)
34
- [country_code, new(definition)]
35
- end.to_h
52
+ address_formats.dup
36
53
  end
37
54
 
38
55
  private
39
56
 
40
- def definitions
41
- @definitions ||= Marshal.load(File.read(File.expand_path("../../../data/address_formats.dump", __FILE__).to_s))
57
+ # Gets the address formats by country code.
58
+ #
59
+ # All are built when the data is loaded, so that every call returns
60
+ # the same instance for a country.
61
+ def address_formats
62
+ Addressing.data_source.fetch("address_formats") do |definitions|
63
+ definitions.to_h do |country_code, definition|
64
+ definition = definition.transform_keys(&:to_sym)
65
+ definition[:default_values] = definition[:default_values].transform_keys(&:to_sym) if definition[:default_values]
66
+
67
+ [country_code, new(process_definition(definition))]
68
+ end
69
+ end
42
70
  end
43
71
 
44
72
  def process_definition(definition)
45
73
  # Merge-in defaults.
46
- definition = generic_definition.merge(definition)
74
+ definition = GENERIC_DEFINITION.merge(definition)
47
75
 
48
76
  # Always require the given name and family name.
49
- definition[:required_fields] << AddressField::GIVEN_NAME
50
- definition[:required_fields] << AddressField::FAMILY_NAME
51
- definition
52
- end
77
+ definition[:required_fields] = definition[:required_fields] | [AddressField::GIVEN_NAME, AddressField::FAMILY_NAME]
53
78
 
54
- def generic_definition
55
- {
56
- format: "%given_name %family_name\n%organization\n%address_line1\n%address_line2\n%address_line3\n%locality",
57
- required_fields: [
58
- "address_line1", "locality"
59
- ],
60
- uppercase_fields: [
61
- "locality"
62
- ],
63
- administrative_area_type: "province",
64
- locality_type: "city",
65
- dependent_locality_type: "suburb",
66
- postal_code_type: "postal",
67
- subdivision_depth: 0
68
- }
79
+ # The address formats are shared, so callers must not be able to
80
+ # change their field lists.
81
+ [:required_fields, :uppercase_fields, :subdivision_fields].each do |key|
82
+ definition[key] = definition[key].dup.freeze
83
+ end
84
+
85
+ definition
69
86
  end
70
87
  end
71
88
 
72
- attr_reader :country_code, :locale, :format, :local_format, :required_fields, :uppercase_fields, :default_values, :administrative_area_type, :locality_type, :dependent_locality_type, :postal_code_type, :postal_code_pattern, :postal_code_prefix, :subdivision_depth
89
+ attr_reader :country_code, :locale, :format, :local_format, :required_fields, :uppercase_fields, :default_values, :administrative_area_type, :locality_type, :dependent_locality_type, :postal_code_type, :postal_code_pattern, :postal_code_prefix
90
+
91
+ # The subdivision fields for which there is predefined subdivision data.
92
+ #
93
+ # This is more precise than #used_subdivision_fields, which returns all
94
+ # subdivision fields used by the format regardless of whether data exists.
95
+ #
96
+ # @return [Array<String>] e.g. ["administrative_area", "locality"]
97
+ attr_reader :subdivision_fields
98
+
99
+ # The number of subdivision fields with predefined data used by the format.
100
+ #
101
+ # @deprecated Use #subdivision_fields instead.
102
+ # @return [Integer]
103
+ def subdivision_depth
104
+ (subdivision_fields & used_subdivision_fields).size
105
+ end
73
106
 
74
107
  def initialize(definition = {})
75
108
  # Validate the presence of required properties.
@@ -87,13 +120,16 @@ module Addressing
87
120
  uppercase_fields: [],
88
121
  default_values: {},
89
122
  postal_code_pattern: nil,
90
- postal_code_prefix: nil,
91
- subdivision_depth: 0
123
+ postal_code_prefix: nil
92
124
  }.merge(definition)
93
125
 
126
+ # Backwards compatibility: derive the subdivision fields from a depth.
127
+ definition[:subdivision_fields] ||= SUBDIVISION_FIELDS.first(definition[:subdivision_depth] || 0)
128
+
94
129
  AddressField.assert_all_exist(definition[:required_fields])
95
130
  AddressField.assert_all_exist(definition[:uppercase_fields])
96
131
  AddressField.assert_all_exist(definition[:default_values].keys)
132
+ AddressField.assert_all_exist(definition[:subdivision_fields])
97
133
 
98
134
  @country_code = definition[:country_code]
99
135
  @locale = definition[:locale]
@@ -102,35 +138,22 @@ module Addressing
102
138
  @required_fields = definition[:required_fields]
103
139
  @uppercase_fields = definition[:uppercase_fields]
104
140
  @default_values = definition[:default_values]
105
- @subdivision_depth = definition[:subdivision_depth]
106
-
107
- if used_fields.include?(AddressField::ADMINISTRATIVE_AREA)
108
- if definition[:administrative_area_type]
109
- AdministrativeAreaType.assert_exists(definition[:administrative_area_type])
110
- @administrative_area_type = definition[:administrative_area_type]
111
- end
112
- end
113
-
114
- if used_fields.include?(AddressField::LOCALITY)
115
- if definition[:locality_type]
116
- LocalityType.assert_exists(definition[:locality_type])
117
- @locality_type = definition[:locality_type]
118
- end
119
- end
120
-
121
- if used_fields.include?(AddressField::DEPENDENT_LOCALITY)
122
- if definition[:dependent_locality_type]
123
- DependentLocalityType.assert_exists(definition[:dependent_locality_type])
124
- @dependent_locality_type = definition[:dependent_locality_type]
125
- end
141
+ @subdivision_fields = definition[:subdivision_fields]
142
+
143
+ # A type is only set when the format uses its field.
144
+ {
145
+ administrative_area_type: [AddressField::ADMINISTRATIVE_AREA, AdministrativeAreaType],
146
+ locality_type: [AddressField::LOCALITY, LocalityType],
147
+ dependent_locality_type: [AddressField::DEPENDENT_LOCALITY, DependentLocalityType],
148
+ postal_code_type: [AddressField::POSTAL_CODE, PostalCodeType]
149
+ }.each do |key, (field, type)|
150
+ next unless definition[key] && used_fields.include?(field)
151
+
152
+ type.assert_exists(definition[key])
153
+ instance_variable_set(:"@#{key}", definition[key])
126
154
  end
127
155
 
128
156
  if used_fields.include?(AddressField::POSTAL_CODE)
129
- if definition[:postal_code_type]
130
- PostalCodeType.assert_exists(definition[:postal_code_type])
131
- @postal_code_type = definition[:postal_code_type]
132
- end
133
-
134
157
  @postal_code_pattern = definition[:postal_code_pattern]
135
158
  @postal_code_prefix = definition[:postal_code_prefix]
136
159
  end
@@ -138,42 +161,15 @@ module Addressing
138
161
 
139
162
  # Gets the list of used fields.
140
163
  def used_fields
141
- @used_fields ||= AddressField.all.filter_map do |key, value|
142
- value if @format.include?("%" + value)
143
- end
164
+ @used_fields ||= AddressField.all.values.select { |field| @format.include?("%#{field}") }
144
165
  end
145
166
 
146
167
  # Gets the list of used subdivision fields.
168
+ #
169
+ # Note that a country might use a subdivision field without having
170
+ # predefined subdivisions for it, see #subdivision_fields.
147
171
  def used_subdivision_fields
148
- fields = [
149
- AddressField::ADMINISTRATIVE_AREA,
150
- AddressField::LOCALITY,
151
- AddressField::DEPENDENT_LOCALITY
152
- ]
153
-
154
- # Remove fields not used by the format.
155
- fields & used_fields
156
- end
157
- end
158
-
159
- class AddressFormatHelper
160
- class << self
161
- # Gets the required fields.
162
- #
163
- # Applies field overrides to the required fields
164
- # specified by the address format.
165
- def required_fields(address_format, field_overrides)
166
- required_fields = address_format.required_fields
167
- required_fields -= field_overrides.optional_fields
168
- required_fields -= field_overrides.hidden_fields
169
-
170
- if field_overrides.required_fields
171
- required_fields += field_overrides.required_fields
172
- required_fields = required_fields.uniq
173
- end
174
-
175
- required_fields
176
- end
172
+ SUBDIVISION_FIELDS & used_fields
177
173
  end
178
174
  end
179
175
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Addressing
4
+ # Validates an address against the address format of its country.
5
+ #
6
+ # @example
7
+ # address = Addressing::Address.new(country_code: "US", administrative_area: "XX")
8
+ # Addressing::AddressValidator.validate(address)
9
+ # # => [#<data Addressing::FieldViolation field=:address_line1, kind=:blank>, ...]
10
+ class AddressValidator
11
+ private_class_method :new
12
+
13
+ class << self
14
+ # Returns the field violations of the address.
15
+ #
16
+ # The rules, in the order in which their violations are reported:
17
+ #
18
+ # 1. Every required field is present. The field overrides make fields
19
+ # optional, hidden or required.
20
+ # 2. Every field that the format does not use, or that is hidden, is blank.
21
+ # 3. The subdivision levels match the predefined subdivisions, from the
22
+ # administrative area down. A hidden level ends the check.
23
+ # 4. The postal code matches the whole pattern, ignoring case. The pattern
24
+ # comes from the last subdivision in the subdivision chain that has
25
+ # one, and otherwise from the address format.
26
+ #
27
+ # An address without a country code gives a single :blank violation on
28
+ # the country code. Without a country there is no address format to
29
+ # check the other fields against.
30
+ #
31
+ # @param address [Address] The address to validate
32
+ # @param field_overrides [FieldOverrides] Overrides of the fields of the address format
33
+ # @param verify_postal_code [Boolean] Whether to check the postal code against its pattern
34
+ # @return [Array<FieldViolation>] The field violations, empty when the address is valid
35
+ def validate(address, field_overrides: FieldOverrides.new({}), verify_postal_code: true)
36
+ return [violation(:country_code, :blank)] if blank?(address.country_code)
37
+
38
+ address_format = AddressFormat.get(address.country_code)
39
+ violations = []
40
+
41
+ required_fields(address_format, field_overrides).each do |field|
42
+ violations << violation(field, :blank) if blank?(address.send(field))
43
+ end
44
+
45
+ used_fields = address_format.used_fields - field_overrides.hidden_fields
46
+
47
+ (AddressField.all.values - used_fields).each do |field|
48
+ violations << violation(field, :present) unless blank?(address.send(field))
49
+ end
50
+
51
+ subdivision_fields = address_format.subdivision_fields
52
+ # A hidden level is not validated, and neither are the levels below it.
53
+ subdivision_values = subdivision_fields.map { |field| address.send(field) unless field_overrides.hidden_fields.include?(field) }
54
+ chain = Subdivision.chain(address_format.country_code, subdivision_values)
55
+
56
+ violations << violation(subdivision_fields[chain.unmatched_level], :invalid) if chain.unmatched_level
57
+
58
+ if verify_postal_code && used_fields.include?(AddressField::POSTAL_CODE) && !valid_postal_code?(address.postal_code, chain.subdivisions, address_format)
59
+ violations << violation(AddressField::POSTAL_CODE, :invalid)
60
+ end
61
+
62
+ violations
63
+ end
64
+
65
+ private
66
+
67
+ def violation(field, kind)
68
+ FieldViolation.new(field: field.to_sym, kind: kind)
69
+ end
70
+
71
+ # Whether the postal code matches the pattern of the last subdivision in
72
+ # the chain that has one, or else the pattern of the address format.
73
+ def valid_postal_code?(postal_code, subdivisions, address_format)
74
+ # Nothing to validate.
75
+ return true if blank?(postal_code)
76
+
77
+ pattern = subdivisions.inject(address_format.postal_code_pattern) do |pattern, subdivision|
78
+ blank?(subdivision.postal_code_pattern) ? pattern : subdivision.postal_code_pattern
79
+ end
80
+ return true unless pattern
81
+
82
+ # The pattern must match the provided value completely.
83
+ match = postal_code.match(Regexp.new(pattern, "i"))
84
+ !match.nil? && match[0] == postal_code
85
+ end
86
+
87
+ # Applies the field overrides to the required fields of the address format.
88
+ def required_fields(address_format, field_overrides)
89
+ required_fields = address_format.required_fields - field_overrides.optional_fields - field_overrides.hidden_fields
90
+ (required_fields + field_overrides.required_fields).uniq
91
+ end
92
+
93
+ def blank?(value)
94
+ Blank.blank?(value)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Addressing
4
+ # Decides whether a value is blank.
5
+ module Blank
6
+ # Matches a value that holds nothing but whitespace, Unicode included.
7
+ PATTERN = /\A[[:space:]]*\z/
8
+ private_constant :PATTERN
9
+
10
+ # Whether the value is blank. Follows ActiveSupport's blank?, and uses it
11
+ # when it is loaded.
12
+ def self.blank?(value)
13
+ return value.blank? if value.respond_to?(:blank?)
14
+
15
+ case value
16
+ when String then blank_string?(value)
17
+ when nil, false then true
18
+ else value.respond_to?(:empty?) && value.empty?
19
+ end
20
+ end
21
+
22
+ def self.blank_string?(value)
23
+ PATTERN.match?(value)
24
+ rescue Encoding::CompatibilityError
25
+ # The value has an encoding that is not compatible with ASCII.
26
+ Regexp.new(PATTERN.source.encode(value.encoding), PATTERN.options | Regexp::FIXEDENCODING).match?(value)
27
+ end
28
+ private_class_method :blank_string?
29
+ end
30
+
31
+ private_constant :Blank
32
+ end