addressing 2.1.0 → 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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +223 -112
  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/BR-AC.json +22 -22
  9. data/data/subdivision/BR-AL.json +102 -102
  10. data/data/subdivision/BR-AM.json +62 -62
  11. data/data/subdivision/BR-AP.json +16 -16
  12. data/data/subdivision/BR-BA.json +417 -417
  13. data/data/subdivision/BR-CE.json +184 -184
  14. data/data/subdivision/BR-DF.json +1 -1
  15. data/data/subdivision/BR-ES.json +79 -79
  16. data/data/subdivision/BR-GO.json +246 -246
  17. data/data/subdivision/BR-MA.json +217 -217
  18. data/data/subdivision/BR-MG.json +853 -853
  19. data/data/subdivision/BR-MS.json +78 -78
  20. data/data/subdivision/BR-MT.json +141 -141
  21. data/data/subdivision/BR-PA.json +144 -144
  22. data/data/subdivision/BR-PB.json +223 -223
  23. data/data/subdivision/BR-PE.json +185 -185
  24. data/data/subdivision/BR-PI.json +223 -223
  25. data/data/subdivision/BR-PR.json +400 -400
  26. data/data/subdivision/BR-RJ.json +93 -93
  27. data/data/subdivision/BR-RN.json +166 -166
  28. data/data/subdivision/BR-RO.json +52 -52
  29. data/data/subdivision/BR-RR.json +15 -15
  30. data/data/subdivision/BR-RS.json +497 -497
  31. data/data/subdivision/BR-SC.json +295 -295
  32. data/data/subdivision/BR-SE.json +75 -75
  33. data/data/subdivision/BR-SP.json +645 -645
  34. data/data/subdivision/BR-TO.json +139 -139
  35. data/data/subdivision/CL-AI.json +10 -10
  36. data/data/subdivision/CL-AN.json +9 -9
  37. data/data/subdivision/CL-AP.json +4 -4
  38. data/data/subdivision/CL-AR.json +32 -32
  39. data/data/subdivision/CL-AT.json +9 -9
  40. data/data/subdivision/CL-BI.json +34 -34
  41. data/data/subdivision/CL-CO.json +15 -15
  42. data/data/subdivision/CL-LI.json +33 -33
  43. data/data/subdivision/CL-LL.json +30 -30
  44. data/data/subdivision/CL-LR.json +12 -12
  45. data/data/subdivision/CL-MA.json +11 -11
  46. data/data/subdivision/CL-ML.json +30 -30
  47. data/data/subdivision/CL-NB.json +21 -21
  48. data/data/subdivision/CL-RM.json +52 -52
  49. data/data/subdivision/CL-TA.json +7 -7
  50. data/data/subdivision/CL-VS.json +38 -38
  51. data/data/subdivision/CV.json +9 -9
  52. data/data/subdivision/KY.json +3 -3
  53. data/data/subdivision/TV.json +1 -1
  54. data/lib/addressing/address.rb +7 -3
  55. data/lib/addressing/address_format.rb +55 -81
  56. data/lib/addressing/address_validator.rb +98 -0
  57. data/lib/addressing/blank.rb +32 -0
  58. data/lib/addressing/country.rb +28 -314
  59. data/lib/addressing/data_source.rb +75 -0
  60. data/lib/addressing/default_formatter.rb +25 -25
  61. data/lib/addressing/enum.rb +2 -6
  62. data/lib/addressing/field_violation.rb +17 -0
  63. data/lib/addressing/lazy_subdivisions.rb +16 -15
  64. data/lib/addressing/locale.rb +20 -269
  65. data/lib/addressing/model.rb +20 -78
  66. data/lib/addressing/postal_label_formatter.rb +16 -15
  67. data/lib/addressing/subdivision.rb +113 -116
  68. data/lib/addressing/version.rb +1 -1
  69. data/lib/addressing.rb +6 -1
  70. metadata +9 -3
  71. data/data/address_formats.dump +0 -0
@@ -17,26 +17,6 @@ module Addressing
17
17
  # list = Addressing::Country.list('en')
18
18
  class Country
19
19
  class << self
20
- AVAILABLE_LOCALES = [
21
- "af", "ak", "am", "ar", "ar-LY", "ar-SA", "as", "az", "ba", "be", "bg",
22
- "bn", "bn-IN", "bs", "ca", "chr", "cs", "cv", "cy", "da", "de",
23
- "de-AT", "de-CH", "dsb", "el", "el-polyton", "en", "en-001", "en-AU",
24
- "en-CA", "en-EE", "en-GE", "en-LT", "en-LV", "en-UA", "es", "es-419",
25
- "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-GT",
26
- "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY", "es-SV",
27
- "es-US", "es-VE", "et", "eu", "fa", "fa-AF", "fi", "fil", "fr",
28
- "fr-BE", "fr-CA", "ga", "gd", "gl", "gu", "he", "hi", "hi-Latn", "hr",
29
- "hsb", "hu", "hy", "id", "ig", "is", "it", "ja", "ka", "kk", "kk-Arab",
30
- "km", "ko", "ko-KP", "kok", "ky", "lo", "lt", "lv", "mk", "ml", "mn",
31
- "mr", "ms", "my", "ne", "nl", "nn", "no", "or", "pa", "pl", "ps",
32
- "ps-PK", "pt", "pt-PT", "qu", "rm", "ro", "ro-MD", "ru", "ru-UA",
33
- "shn", "si", "sk", "sl", "so", "sq", "sr", "sr-Cyrl-BA", "sr-Cyrl-ME",
34
- "sr-Cyrl-XK", "sr-Latn", "sr-Latn-BA", "sr-Latn-ME", "sr-Latn-XK",
35
- "sv", "sw", "sw-CD", "sw-KE", "ta", "te", "th", "tk", "tr", "uk", "ur",
36
- "ur-IN", "uz", "vi", "yue", "yue-Hans", "zh", "zh-Hant", "zh-Hant-HK",
37
- "zu"
38
- ]
39
-
40
20
  # Gets a Country instance for the provided country code.
41
21
  #
42
22
  # @param country_code [String] ISO 3166-1 alpha-2 country code
@@ -49,17 +29,9 @@ module Addressing
49
29
 
50
30
  raise UnknownCountryError.new(country_code) unless base_definitions.key?(country_code)
51
31
 
52
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
53
- definitions = load_definitions(locale)
32
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
54
33
 
55
- new(
56
- country_code: country_code,
57
- name: definitions[country_code],
58
- three_letter_code: base_definitions[country_code][0],
59
- numeric_code: base_definitions[country_code][1],
60
- currency_code: base_definitions[country_code][2],
61
- locale: locale
62
- )
34
+ build(country_code, load_definitions(locale)[country_code], locale)
63
35
  end
64
36
 
65
37
  # Gets all Country instances.
@@ -68,21 +40,11 @@ module Addressing
68
40
  # @param fallback_locale [String] Fallback locale if requested locale is unavailable
69
41
  # @return [Hash<String, Country>] Hash of country code => Country instance
70
42
  def all(locale = "en", fallback_locale = "en")
71
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
72
- definitions = load_definitions(locale)
73
-
74
- definitions.map do |country_code, country_name|
75
- country = new(
76
- country_code: country_code,
77
- name: country_name,
78
- three_letter_code: base_definitions[country_code][0],
79
- numeric_code: base_definitions[country_code][1],
80
- currency_code: base_definitions[country_code][2],
81
- locale: locale
82
- )
43
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
83
44
 
84
- [country_code, country]
85
- end.to_h
45
+ load_definitions(locale).to_h do |country_code, country_name|
46
+ [country_code, build(country_code, country_name, locale)]
47
+ end
86
48
  end
87
49
 
88
50
  # Gets a list of country codes and names.
@@ -91,289 +53,41 @@ module Addressing
91
53
  # @param fallback_locale [String] Fallback locale if requested locale is unavailable
92
54
  # @return [Hash<String, String>] Hash of country code => country name
93
55
  def list(locale = "en", fallback_locale = "en")
94
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
95
- definitions = load_definitions(locale)
56
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
96
57
 
97
- definitions.map do |country_code, country_name|
98
- [country_code, country_name]
99
- end.to_h
58
+ load_definitions(locale).dup
100
59
  end
101
60
 
102
61
  protected
103
62
 
63
+ def build(country_code, name, locale)
64
+ base_definition = base_definitions[country_code]
65
+
66
+ new(
67
+ country_code: country_code,
68
+ name: name,
69
+ three_letter_code: base_definition["three_letter_code"],
70
+ numeric_code: base_definition["numeric_code"],
71
+ currency_code: base_definition["currency_code"],
72
+ locale: locale
73
+ )
74
+ end
75
+
104
76
  # Loads the country definitions for the provided locale.
105
77
  def load_definitions(locale)
106
- @definitions ||= {}
107
- unless @definitions.key?(locale)
108
- filename = File.join(File.expand_path("../../../data/country", __FILE__).to_s, "#{locale}.json")
109
- @definitions[locale] = JSON.parse(File.read(filename))
110
- end
78
+ Addressing.data_source.fetch("country/#{locale}")
79
+ end
111
80
 
112
- @definitions[locale]
81
+ # Gets the locales that have country names, one data file each.
82
+ def available_locales
83
+ Addressing.data_source.names("country")
113
84
  end
114
85
 
115
86
  # Gets the base country definitions.
116
87
  #
117
- # Contains data common to all locales: three letter code, numeric code.
88
+ # Contains data common to all locales: three letter code, numeric code, currency code.
118
89
  def base_definitions
119
- @base_definitions ||= {
120
- "AC" => ["ASC", nil, "SHP"],
121
- "AD" => ["AND", "020", "EUR"],
122
- "AE" => ["ARE", "784", "AED"],
123
- "AF" => ["AFG", "004", "AFN"],
124
- "AG" => ["ATG", "028", "XCD"],
125
- "AI" => ["AIA", "660", "XCD"],
126
- "AL" => ["ALB", "008", "ALL"],
127
- "AM" => ["ARM", "051", "AMD"],
128
- "AO" => ["AGO", "024", "AOA"],
129
- "AQ" => ["ATA", "010", nil],
130
- "AR" => ["ARG", "032", "ARS"],
131
- "AS" => ["ASM", "016", "USD"],
132
- "AT" => ["AUT", "040", "EUR"],
133
- "AU" => ["AUS", "036", "AUD"],
134
- "AW" => ["ABW", "533", "AWG"],
135
- "AX" => ["ALA", "248", "EUR"],
136
- "AZ" => ["AZE", "031", "AZN"],
137
- "BA" => ["BIH", "070", "BAM"],
138
- "BB" => ["BRB", "052", "BBD"],
139
- "BD" => ["BGD", "050", "BDT"],
140
- "BE" => ["BEL", "056", "EUR"],
141
- "BF" => ["BFA", "854", "XOF"],
142
- "BG" => ["BGR", "100", "EUR"],
143
- "BH" => ["BHR", "048", "BHD"],
144
- "BI" => ["BDI", "108", "BIF"],
145
- "BJ" => ["BEN", "204", "XOF"],
146
- "BL" => ["BLM", "652", "EUR"],
147
- "BM" => ["BMU", "060", "BMD"],
148
- "BN" => ["BRN", "096", "BND"],
149
- "BO" => ["BOL", "068", "BOB"],
150
- "BQ" => ["BES", "535", "USD"],
151
- "BR" => ["BRA", "076", "BRL"],
152
- "BS" => ["BHS", "044", "BSD"],
153
- "BT" => ["BTN", "064", "BTN"],
154
- "BV" => ["BVT", "074", "NOK"],
155
- "BW" => ["BWA", "072", "BWP"],
156
- "BY" => ["BLR", "112", "BYN"],
157
- "BZ" => ["BLZ", "084", "BZD"],
158
- "CA" => ["CAN", "124", "CAD"],
159
- "CC" => ["CCK", "166", "AUD"],
160
- "CD" => ["COD", "180", "CDF"],
161
- "CF" => ["CAF", "140", "XAF"],
162
- "CG" => ["COG", "178", "XAF"],
163
- "CH" => ["CHE", "756", "CHF"],
164
- "CI" => ["CIV", "384", "XOF"],
165
- "CK" => ["COK", "184", "NZD"],
166
- "CL" => ["CHL", "152", "CLP"],
167
- "CM" => ["CMR", "120", "XAF"],
168
- "CN" => ["CHN", "156", "CNY"],
169
- "CO" => ["COL", "170", "COP"],
170
- "CP" => ["CPT", nil, nil],
171
- "CR" => ["CRI", "188", "CRC"],
172
- "CU" => ["CUB", "192", "CUP"],
173
- "CV" => ["CPV", "132", "CVE"],
174
- "CW" => ["CUW", "531", "XCG"],
175
- "CX" => ["CXR", "162", "AUD"],
176
- "CY" => ["CYP", "196", "EUR"],
177
- "CZ" => ["CZE", "203", "CZK"],
178
- "DE" => ["DEU", "276", "EUR"],
179
- "DG" => ["DGA", nil, "USD"],
180
- "DJ" => ["DJI", "262", "DJF"],
181
- "DK" => ["DNK", "208", "DKK"],
182
- "DM" => ["DMA", "212", "XCD"],
183
- "DO" => ["DOM", "214", "DOP"],
184
- "DZ" => ["DZA", "012", "DZD"],
185
- "EA" => [nil, nil, "EUR"],
186
- "EC" => ["ECU", "218", "USD"],
187
- "EE" => ["EST", "233", "EUR"],
188
- "EG" => ["EGY", "818", "EGP"],
189
- "EH" => ["ESH", "732", "MAD"],
190
- "ER" => ["ERI", "232", "ERN"],
191
- "ES" => ["ESP", "724", "EUR"],
192
- "ET" => ["ETH", "231", "ETB"],
193
- "FI" => ["FIN", "246", "EUR"],
194
- "FJ" => ["FJI", "242", "FJD"],
195
- "FK" => ["FLK", "238", "FKP"],
196
- "FM" => ["FSM", "583", "USD"],
197
- "FO" => ["FRO", "234", "DKK"],
198
- "FR" => ["FRA", "250", "EUR"],
199
- "GA" => ["GAB", "266", "XAF"],
200
- "GB" => ["GBR", "826", "GBP"],
201
- "GD" => ["GRD", "308", "XCD"],
202
- "GE" => ["GEO", "268", "GEL"],
203
- "GF" => ["GUF", "254", "EUR"],
204
- "GG" => ["GGY", "831", "GBP"],
205
- "GH" => ["GHA", "288", "GHS"],
206
- "GI" => ["GIB", "292", "GIP"],
207
- "GL" => ["GRL", "304", "DKK"],
208
- "GM" => ["GMB", "270", "GMD"],
209
- "GN" => ["GIN", "324", "GNF"],
210
- "GP" => ["GLP", "312", "EUR"],
211
- "GQ" => ["GNQ", "226", "XAF"],
212
- "GR" => ["GRC", "300", "EUR"],
213
- "GS" => ["SGS", "239", "GBP"],
214
- "GT" => ["GTM", "320", "GTQ"],
215
- "GU" => ["GUM", "316", "USD"],
216
- "GW" => ["GNB", "624", "XOF"],
217
- "GY" => ["GUY", "328", "GYD"],
218
- "HK" => ["HKG", "344", "HKD"],
219
- "HM" => ["HMD", "334", "AUD"],
220
- "HN" => ["HND", "340", "HNL"],
221
- "HR" => ["HRV", "191", "EUR"],
222
- "HT" => ["HTI", "332", "USD"],
223
- "HU" => ["HUN", "348", "HUF"],
224
- "IC" => [nil, nil, "EUR"],
225
- "ID" => ["IDN", "360", "IDR"],
226
- "IE" => ["IRL", "372", "EUR"],
227
- "IL" => ["ISR", "376", "ILS"],
228
- "IM" => ["IMN", "833", "GBP"],
229
- "IN" => ["IND", "356", "INR"],
230
- "IO" => ["IOT", "086", "USD"],
231
- "IQ" => ["IRQ", "368", "IQD"],
232
- "IR" => ["IRN", "364", "IRR"],
233
- "IS" => ["ISL", "352", "ISK"],
234
- "IT" => ["ITA", "380", "EUR"],
235
- "JE" => ["JEY", "832", "GBP"],
236
- "JM" => ["JAM", "388", "JMD"],
237
- "JO" => ["JOR", "400", "JOD"],
238
- "JP" => ["JPN", "392", "JPY"],
239
- "KE" => ["KEN", "404", "KES"],
240
- "KG" => ["KGZ", "417", "KGS"],
241
- "KH" => ["KHM", "116", "KHR"],
242
- "KI" => ["KIR", "296", "AUD"],
243
- "KM" => ["COM", "174", "KMF"],
244
- "KN" => ["KNA", "659", "XCD"],
245
- "KP" => ["PRK", "408", "KPW"],
246
- "KR" => ["KOR", "410", "KRW"],
247
- "KW" => ["KWT", "414", "KWD"],
248
- "KY" => ["CYM", "136", "KYD"],
249
- "KZ" => ["KAZ", "398", "KZT"],
250
- "LA" => ["LAO", "418", "LAK"],
251
- "LB" => ["LBN", "422", "LBP"],
252
- "LC" => ["LCA", "662", "XCD"],
253
- "LI" => ["LIE", "438", "CHF"],
254
- "LK" => ["LKA", "144", "LKR"],
255
- "LR" => ["LBR", "430", "LRD"],
256
- "LS" => ["LSO", "426", "LSL"],
257
- "LT" => ["LTU", "440", "EUR"],
258
- "LU" => ["LUX", "442", "EUR"],
259
- "LV" => ["LVA", "428", "EUR"],
260
- "LY" => ["LBY", "434", "LYD"],
261
- "MA" => ["MAR", "504", "MAD"],
262
- "MC" => ["MCO", "492", "EUR"],
263
- "MD" => ["MDA", "498", "MDL"],
264
- "ME" => ["MNE", "499", "EUR"],
265
- "MF" => ["MAF", "663", "EUR"],
266
- "MG" => ["MDG", "450", "MGA"],
267
- "MH" => ["MHL", "584", "USD"],
268
- "MK" => ["MKD", "807", "MKD"],
269
- "ML" => ["MLI", "466", nil],
270
- "MM" => ["MMR", "104", "MMK"],
271
- "MN" => ["MNG", "496", "MNT"],
272
- "MO" => ["MAC", "446", "MOP"],
273
- "MP" => ["MNP", "580", "USD"],
274
- "MQ" => ["MTQ", "474", "EUR"],
275
- "MR" => ["MRT", "478", "MRU"],
276
- "MS" => ["MSR", "500", "XCD"],
277
- "MT" => ["MLT", "470", "EUR"],
278
- "MU" => ["MUS", "480", "MUR"],
279
- "MV" => ["MDV", "462", "MVR"],
280
- "MW" => ["MWI", "454", "MWK"],
281
- "MX" => ["MEX", "484", "MXN"],
282
- "MY" => ["MYS", "458", "MYR"],
283
- "MZ" => ["MOZ", "508", "MZN"],
284
- "NA" => ["NAM", "516", "NAD"],
285
- "NC" => ["NCL", "540", "XPF"],
286
- "NE" => ["NER", "562", "XOF"],
287
- "NF" => ["NFK", "574", "AUD"],
288
- "NG" => ["NGA", "566", "NGN"],
289
- "NI" => ["NIC", "558", "NIO"],
290
- "NL" => ["NLD", "528", "EUR"],
291
- "NO" => ["NOR", "578", "NOK"],
292
- "NP" => ["NPL", "524", "NPR"],
293
- "NR" => ["NRU", "520", "AUD"],
294
- "NU" => ["NIU", "570", "NZD"],
295
- "NZ" => ["NZL", "554", "NZD"],
296
- "OM" => ["OMN", "512", "OMR"],
297
- "PA" => ["PAN", "591", "USD"],
298
- "PE" => ["PER", "604", "PEN"],
299
- "PF" => ["PYF", "258", "XPF"],
300
- "PG" => ["PNG", "598", "PGK"],
301
- "PH" => ["PHL", "608", "PHP"],
302
- "PK" => ["PAK", "586", "PKR"],
303
- "PL" => ["POL", "616", "PLN"],
304
- "PM" => ["SPM", "666", "EUR"],
305
- "PN" => ["PCN", "612", "NZD"],
306
- "PR" => ["PRI", "630", "USD"],
307
- "PS" => ["PSE", "275", "ILS"],
308
- "PT" => ["PRT", "620", "EUR"],
309
- "PW" => ["PLW", "585", "USD"],
310
- "PY" => ["PRY", "600", "PYG"],
311
- "QA" => ["QAT", "634", "QAR"],
312
- "RE" => ["REU", "638", "EUR"],
313
- "RO" => ["ROU", "642", "RON"],
314
- "RS" => ["SRB", "688", "RSD"],
315
- "RU" => ["RUS", "643", "RUB"],
316
- "RW" => ["RWA", "646", "RWF"],
317
- "SA" => ["SAU", "682", "SAR"],
318
- "SB" => ["SLB", "090", "SBD"],
319
- "SC" => ["SYC", "690", "SCR"],
320
- "SD" => ["SDN", "729", "SDG"],
321
- "SE" => ["SWE", "752", "SEK"],
322
- "SG" => ["SGP", "702", "SGD"],
323
- "SH" => ["SHN", "654", "SHP"],
324
- "SI" => ["SVN", "705", "EUR"],
325
- "SJ" => ["SJM", "744", "NOK"],
326
- "SK" => ["SVK", "703", "EUR"],
327
- "SL" => ["SLE", "694", "SLE"],
328
- "SM" => ["SMR", "674", "EUR"],
329
- "SN" => ["SEN", "686", "XOF"],
330
- "SO" => ["SOM", "706", "SOS"],
331
- "SR" => ["SUR", "740", "SRD"],
332
- "SS" => ["SSD", "728", "SSP"],
333
- "ST" => ["STP", "678", "STN"],
334
- "SV" => ["SLV", "222", "USD"],
335
- "SX" => ["SXM", "534", "XCG"],
336
- "SY" => ["SYR", "760", "SYP"],
337
- "SZ" => ["SWZ", "748", "SZL"],
338
- "TA" => ["TAA", nil, "GBP"],
339
- "TC" => ["TCA", "796", "USD"],
340
- "TD" => ["TCD", "148", "XAF"],
341
- "TF" => ["ATF", "260", "EUR"],
342
- "TG" => ["TGO", "768", "XOF"],
343
- "TH" => ["THA", "764", "THB"],
344
- "TJ" => ["TJK", "762", "TJS"],
345
- "TK" => ["TKL", "772", "NZD"],
346
- "TL" => ["TLS", "626", "USD"],
347
- "TM" => ["TKM", "795", "TMT"],
348
- "TN" => ["TUN", "788", "TND"],
349
- "TO" => ["TON", "776", "TOP"],
350
- "TR" => ["TUR", "792", "TRY"],
351
- "TT" => ["TTO", "780", "TTD"],
352
- "TV" => ["TUV", "798", "AUD"],
353
- "TW" => ["TWN", "158", "TWD"],
354
- "TZ" => ["TZA", "834", "TZS"],
355
- "UA" => ["UKR", "804", "UAH"],
356
- "UG" => ["UGA", "800", "UGX"],
357
- "UM" => ["UMI", "581", "USD"],
358
- "US" => ["USA", "840", "USD"],
359
- "UY" => ["URY", "858", "UYU"],
360
- "UZ" => ["UZB", "860", "UZS"],
361
- "VA" => ["VAT", "336", "EUR"],
362
- "VC" => ["VCT", "670", "XCD"],
363
- "VE" => ["VEN", "862", "VES"],
364
- "VG" => ["VGB", "092", "USD"],
365
- "VI" => ["VIR", "850", "USD"],
366
- "VN" => ["VNM", "704", "VND"],
367
- "VU" => ["VUT", "548", "VUV"],
368
- "WF" => ["WLF", "876", "XPF"],
369
- "WS" => ["WSM", "882", "WST"],
370
- "XK" => ["XKK", "983", "EUR"],
371
- "YE" => ["YEM", "887", "YER"],
372
- "YT" => ["MYT", "175", "EUR"],
373
- "ZA" => ["ZAF", "710", "ZAR"],
374
- "ZM" => ["ZMB", "894", "ZMW"],
375
- "ZW" => ["ZWE", "716", "ZWG"]
376
- }
90
+ Addressing.data_source.fetch("countries")
377
91
  end
378
92
  end
379
93
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Addressing
4
+ class << self
5
+ # The data source that the reference data is read from.
6
+ #
7
+ # @api private
8
+ attr_writer :data_source
9
+
10
+ # @api private
11
+ # @return [DataSource]
12
+ def data_source
13
+ @data_source ||= DataSource.new(DataSource::DEFAULT_DIR)
14
+ end
15
+ end
16
+
17
+ # Reads the reference data: the JSON files that the data sync writes.
18
+ #
19
+ # Each instance has its own cache, so a new instance reads the files again.
20
+ #
21
+ # @api private
22
+ class DataSource
23
+ # The data directory that ships with the gem.
24
+ DEFAULT_DIR = File.expand_path("../../data", __dir__)
25
+
26
+ def initialize(dir)
27
+ @dir = dir
28
+ @cache = {}
29
+ end
30
+
31
+ # Gets the dataset with the provided name.
32
+ #
33
+ # The parsed JSON is passed to the block, and the result of the block is
34
+ # cached, so each dataset is read and processed once per data source.
35
+ # A missing dataset gives nil, which is cached as well. Malformed JSON
36
+ # raises, because the data files are never edited by hand.
37
+ #
38
+ # @param name [String] Path relative to the data directory, without extension (e.g. "country/en")
39
+ # @yieldparam json [Hash, Array] The parsed dataset
40
+ # @return [Object, nil] The result of the block, or the parsed dataset without a block
41
+ # @raise [JSON::ParserError] if the dataset is malformed
42
+ def fetch(name)
43
+ return @cache[name] if @cache.key?(name)
44
+
45
+ dir, basename = File.split(name)
46
+ return @cache[name] = nil unless filenames(dir).include?("#{basename}.json")
47
+
48
+ json = JSON.parse(File.read(File.join(@dir, "#{name}.json"), encoding: "UTF-8"))
49
+ @cache[name] = block_given? ? yield(json) : json
50
+ end
51
+
52
+ # Gets the names of the datasets in a directory, without the directory.
53
+ #
54
+ # @param dir [String] Directory relative to the data directory (e.g. "country")
55
+ # @return [Array<String>] Sorted names (e.g. ["af", "ak", ...])
56
+ def names(dir)
57
+ (@names ||= {})[dir] ||= filenames(dir).filter_map { |filename| File.basename(filename, ".json") if filename.end_with?(".json") }.sort.freeze
58
+ end
59
+
60
+ private
61
+
62
+ # Gets the file names in a directory of the data directory.
63
+ #
64
+ # A dataset exists only when its name matches a file name exactly, so
65
+ # that a lookup gives the same result on a case-insensitive file system.
66
+ # The listing is cached like the datasets, so a file added later is not
67
+ # seen by this instance.
68
+ def filenames(dir)
69
+ (@filenames ||= {})[dir] ||= begin
70
+ path = File.join(@dir, dir)
71
+ Dir.exist?(path) ? Dir.children(path).to_set : Set.new
72
+ end
73
+ end
74
+ end
75
+ end
@@ -7,6 +7,7 @@ module Addressing
7
7
  FORMAT_SPLIT_PATTERN = /(#{FORMAT_PLACEHOLDER_PATTERN})/
8
8
  LEADING_TRAILING_PUNCTUATION_PATTERN = /\A[ \-,]+|[ \-,]+\z/
9
9
  MULTIPLE_SPACES_PATTERN = /\s\s+/
10
+ HTML_TAG_PATTERN = /\A[a-z][a-z0-9-]*\z/i
10
11
 
11
12
  DEFAULT_OPTIONS = {
12
13
  locale: DEFAULT_LOCALE,
@@ -23,9 +24,8 @@ module Addressing
23
24
  end
24
25
 
25
26
  def format(address, options = {})
26
- assert_options(options)
27
-
28
27
  options = @default_options.merge(options)
28
+ assert_options(options)
29
29
  address_format = AddressFormat.get(address.country_code)
30
30
 
31
31
  # Add the country to the bottom or the top of the format string,
@@ -39,7 +39,7 @@ module Addressing
39
39
  view = build_view(address, address_format, options)
40
40
  view = render_view(view)
41
41
 
42
- replacements = view.map { |key, element| ["%#{key}", element] }.to_h
42
+ replacements = view.transform_keys { |key| "%#{key}" }
43
43
  output = insert_values(format_string, replacements)
44
44
  output = clean_output(output)
45
45
 
@@ -57,12 +57,12 @@ module Addressing
57
57
  # Builds the view for the given address.
58
58
  def build_view(address, address_format, options)
59
59
  countries = country_list(options[:locale])
60
- values = values(address, address_format).merge({"country" => countries.key?(address.country_code) ? countries[address.country_code] : address.country_code})
60
+ values = values(address, address_format).merge("country" => countries.fetch(address.country_code, address.country_code))
61
61
  used_fields = address_format.used_fields + ["country"]
62
62
 
63
- used_fields.map do |field|
63
+ used_fields.to_h do |field|
64
64
  [field, {html: options[:html], html_tag: "span", html_attributes: {class: field.tr("_", "-")}, value: values[field]}]
65
- end.to_h
65
+ end
66
66
  end
67
67
 
68
68
  # Gets the country list for a locale, with caching.
@@ -72,16 +72,16 @@ module Addressing
72
72
 
73
73
  # Renders the given view.
74
74
  def render_view(view)
75
- view.map do |key, element|
76
- next [key, ""] if element[:value].empty?
75
+ view.transform_values do |element|
76
+ next "" if element[:value].empty?
77
77
 
78
78
  if element[:html]
79
79
  element[:value] = CGI.escapeHTML(element[:value])
80
- next [key, render_html_element(element)]
80
+ next render_html_element(element)
81
81
  end
82
82
 
83
- [key, element[:value].gsub(/<\/?[^>]*>/, "")]
84
- end.to_h
83
+ element[:value].gsub(/<\/?[^>]*>/, "")
84
+ end
85
85
  end
86
86
 
87
87
  def render_html_element(element)
@@ -147,36 +147,27 @@ module Addressing
147
147
 
148
148
  # Extracts all address field values.
149
149
  def extract_address_values(address)
150
- AddressField.all.map { |_, field| [field, address.send(field)] }.to_h
150
+ AddressField.all.values.to_h { |field| [field, address.send(field)] }
151
151
  end
152
152
 
153
153
  # Replaces the subdivision values with the codes of any predefined ones.
154
154
  def resolve_subdivision_values(values, address, address_format)
155
- parents = [address.country_code]
156
-
157
- address_format.subdivision_fields.each do |field|
158
- # This level is empty, so there can be no sublevels.
159
- break if values[field].nil? || values[field].empty?
155
+ fields = address_format.subdivision_fields
156
+ chain = Subdivision.chain(address.country_code, values.values_at(*fields))
160
157
 
161
- subdivision = Subdivision.get(values[field], parents)
158
+ fields.zip(chain.subdivisions) do |field, subdivision|
162
159
  break if subdivision.nil?
163
160
 
164
- parents << values[field]
165
-
166
161
  # Replace the value with the expected code.
167
162
  use_local_name = Locale.match_candidates(address.locale, subdivision.locale)
168
163
  values[field] = use_local_name ? subdivision.local_code : subdivision.code
169
-
170
- # The current subdivision has no children, stop.
171
- break unless subdivision.children?
172
164
  end
173
165
  end
174
166
 
175
- private
176
-
177
167
  # Validates the provided options.
178
168
  #
179
169
  # Ensures the absence of unknown keys, correct data types and values.
170
+ # A subclass with more options extends this and calls super.
180
171
  def assert_options(options)
181
172
  options.each do |option, value|
182
173
  unless self.class::DEFAULT_OPTIONS.key?(option)
@@ -191,6 +182,15 @@ module Addressing
191
182
  if options.key?(:html_attributes) && !options[:html_attributes].is_a?(Hash)
192
183
  raise ArgumentError, "The option `html_attributes` must be a hash."
193
184
  end
185
+
186
+ # The tag is written into the markup as is, so only allow a tag name.
187
+ if options.key?(:html_tag) && !HTML_TAG_PATTERN.match?(options[:html_tag].to_s)
188
+ raise ArgumentError, "The option `html_tag` must be an HTML tag name."
189
+ end
190
+
191
+ if options.key?(:locale) && !options[:locale].is_a?(String)
192
+ raise ArgumentError, "The option `locale` must be a string."
193
+ end
194
194
  end
195
195
  end
196
196
  end
@@ -5,12 +5,8 @@ module Addressing
5
5
  class << self
6
6
  # Gets all available values.
7
7
  def all
8
- @values ||= {}
9
- if !@values.key?(name)
10
- @values[name] = constants.map { |constant| [constant, const_get(constant)] }.to_h
11
- end
12
-
13
- @values[name]
8
+ # Each subclass has its own @all.
9
+ @all ||= constants.to_h { |constant| [constant, const_get(constant)] }
14
10
  end
15
11
 
16
12
  # Gets the key of the provided value.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Addressing
4
+ # One field of an address that breaks a rule of the address format for its
5
+ # country, together with the kind of rule it breaks. A blank country code is
6
+ # also a field violation.
7
+ #
8
+ # field is the field as a symbol, such as :postal_code. kind is one of:
9
+ #
10
+ # - :blank, the field must not be blank.
11
+ # - :present, the field must be blank.
12
+ # - :invalid, the value matches no predefined subdivision or does not match
13
+ # the postal code pattern.
14
+ #
15
+ # The kinds match the error types of Rails.
16
+ FieldViolation = Data.define(:field, :kind)
17
+ end