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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +223 -111
- data/data/UPSTREAM_VERSION +1 -0
- data/data/address_formats.json +2256 -206
- data/data/countries.json +1282 -0
- data/data/locale.json +222 -0
- data/data/subdivision/AE.json +1 -1
- data/data/subdivision/BR-AC.json +22 -22
- data/data/subdivision/BR-AL.json +102 -102
- data/data/subdivision/BR-AM.json +62 -62
- data/data/subdivision/BR-AP.json +16 -16
- data/data/subdivision/BR-BA.json +417 -417
- data/data/subdivision/BR-CE.json +184 -184
- data/data/subdivision/BR-DF.json +1 -1
- data/data/subdivision/BR-ES.json +79 -79
- data/data/subdivision/BR-GO.json +246 -246
- data/data/subdivision/BR-MA.json +217 -217
- data/data/subdivision/BR-MG.json +853 -853
- data/data/subdivision/BR-MS.json +78 -78
- data/data/subdivision/BR-MT.json +141 -141
- data/data/subdivision/BR-PA.json +144 -144
- data/data/subdivision/BR-PB.json +223 -223
- data/data/subdivision/BR-PE.json +185 -185
- data/data/subdivision/BR-PI.json +223 -223
- data/data/subdivision/BR-PR.json +400 -400
- data/data/subdivision/BR-RJ.json +93 -93
- data/data/subdivision/BR-RN.json +166 -166
- data/data/subdivision/BR-RO.json +52 -52
- data/data/subdivision/BR-RR.json +15 -15
- data/data/subdivision/BR-RS.json +497 -497
- data/data/subdivision/BR-SC.json +295 -295
- data/data/subdivision/BR-SE.json +75 -75
- data/data/subdivision/BR-SP.json +645 -645
- data/data/subdivision/BR-TO.json +139 -139
- data/data/subdivision/CA.json +6 -12
- data/data/subdivision/CL-AI.json +10 -10
- data/data/subdivision/CL-AN.json +9 -9
- data/data/subdivision/CL-AP.json +4 -4
- data/data/subdivision/CL-AR.json +32 -32
- data/data/subdivision/CL-AT.json +9 -9
- data/data/subdivision/CL-BI.json +34 -34
- data/data/subdivision/CL-CO.json +15 -15
- data/data/subdivision/CL-LI.json +33 -33
- data/data/subdivision/CL-LL.json +30 -30
- data/data/subdivision/CL-LR.json +12 -12
- data/data/subdivision/CL-MA.json +11 -11
- data/data/subdivision/CL-ML.json +30 -30
- data/data/subdivision/CL-NB.json +21 -21
- data/data/subdivision/CL-RM.json +52 -52
- data/data/subdivision/CL-TA.json +7 -7
- data/data/subdivision/CL-VS.json +38 -38
- data/data/subdivision/CO.json +1 -1
- data/data/subdivision/CV.json +9 -9
- data/data/subdivision/GB.json +675 -0
- data/data/subdivision/GT.json +71 -0
- data/data/subdivision/IT.json +13 -1
- data/data/subdivision/KY.json +3 -3
- data/data/subdivision/NG.json +1 -1
- data/data/subdivision/RU.json +4 -4
- data/data/subdivision/TH.json +7 -7
- data/data/subdivision/TR.json +1 -1
- data/data/subdivision/TV.json +1 -1
- data/data/subdivision/VE.json +4 -4
- data/data/subdivision/VN.json +1 -1
- data/lib/addressing/address.rb +7 -3
- data/lib/addressing/address_format.rb +93 -97
- data/lib/addressing/address_validator.rb +98 -0
- data/lib/addressing/blank.rb +32 -0
- data/lib/addressing/country.rb +28 -314
- data/lib/addressing/data_source.rb +75 -0
- data/lib/addressing/default_formatter.rb +61 -33
- data/lib/addressing/enum.rb +2 -6
- data/lib/addressing/field_violation.rb +17 -0
- data/lib/addressing/lazy_subdivisions.rb +16 -15
- data/lib/addressing/locale.rb +20 -269
- data/lib/addressing/model.rb +20 -79
- data/lib/addressing/postal_label_formatter.rb +16 -15
- data/lib/addressing/subdivision.rb +121 -106
- data/lib/addressing/version.rb +1 -1
- data/lib/addressing.rb +6 -1
- metadata +11 -3
- 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
|
+
}
|
data/data/subdivision/IT.json
CHANGED
|
@@ -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": "
|
|
374
|
+
"name": "Sulcis Iglesiente"
|
|
363
375
|
},
|
|
364
376
|
"TA": {
|
|
365
377
|
"code": "TA",
|
data/data/subdivision/KY.json
CHANGED
data/data/subdivision/NG.json
CHANGED
data/data/subdivision/RU.json
CHANGED
|
@@ -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": "Ульяновская область"
|
data/data/subdivision/TH.json
CHANGED
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
270
|
+
"name": "Suphan Buri",
|
|
271
271
|
"local_name": "สุพรรณบุรี"
|
|
272
272
|
},
|
|
273
273
|
"84": {
|
data/data/subdivision/TR.json
CHANGED
data/data/subdivision/TV.json
CHANGED
data/data/subdivision/VE.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"name": "Dependencias Federales"
|
|
33
33
|
},
|
|
34
34
|
"A": {
|
|
35
|
-
"name": "Distrito
|
|
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
|
},
|
data/data/subdivision/VN.json
CHANGED
data/lib/addressing/address.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
12
|
-
# format.required_fields
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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 =
|
|
74
|
+
definition = GENERIC_DEFINITION.merge(definition)
|
|
47
75
|
|
|
48
76
|
# Always require the given name and family name.
|
|
49
|
-
definition[:required_fields]
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
@
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
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
|
-
|
|
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
|