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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49688e426e19464f0e7a96b897e4edced4c46a194303bb8a381c5ffb897d1d4f
4
- data.tar.gz: 52d8f2a6f085cad074dddedcba8e4218ce2f2ec9310789b898fbf8ff04fab435
3
+ metadata.gz: 3960270871e0f9694221dafc7ba2786b299a50508de02d13d6d4bd4c5a0f4a4c
4
+ data.tar.gz: dcb37a8e3a0f402b90c3e29afb0ea296c176e32f375d58755be1984616869a44
5
5
  SHA512:
6
- metadata.gz: 190afd7e32d51607270b3866c33f2268379bcd3ea9528f223f48113bd277940704b4ae3e9353e34cbec05e5a80545e64fa52154dfcdf6fd6540cdbaa4feef34a
7
- data.tar.gz: 0fd91253ba248a1a1395f7159cef755abb8082159da02712b4e91f09eec138cc92ff25b57d1a395505a44e455936c037ec3441d994e127cfd6d54174d398f6a1
6
+ metadata.gz: 3f6c2fe3c4c2fb6aa90b2225953348ba02b60026786cde2365028f50e5015688dbfa8d0b373ce5393b7db45863f26de8086ebffe5bb71fa6105fb3ce8db000b3
7
+ data.tar.gz: 7bc0d61fa1d7fd811c951e191edf5cf89702233c067645f03716da959d437cd32314c895d99984fee9552f5488cf907b65af9556cd7b05f13113c494afd478a7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to `addressing` will be documented in this file.
4
4
 
5
+ ## [2.2.0](https://github.com/robinvdvleuten/addressing/compare/v2.1.0...v2.2.0) (2026-10-01)
6
+
7
+
8
+ ### Features
9
+
10
+ * add AddressValidator, which validates a plain address ([2d1b8fe](https://github.com/robinvdvleuten/addressing/commit/2d1b8fe8e7b501d8950a61349fa400bd1687d353)), closes [#52](https://github.com/robinvdvleuten/addressing/issues/52)
11
+ * add FieldViolation, one field of an address that breaks a rule of its address format ([2d1b8fe](https://github.com/robinvdvleuten/addressing/commit/2d1b8fe8e7b501d8950a61349fa400bd1687d353))
12
+ * remove the undocumented AddressFormatHelper class, whose required fields rule moved into AddressValidator ([2d1b8fe](https://github.com/robinvdvleuten/addressing/commit/2d1b8fe8e7b501d8950a61349fa400bd1687d353))
13
+ * remove the undocumented verify_address_format, verify_subdivisions and verify_postal_code methods from models that call validates_address_format ([2d1b8fe](https://github.com/robinvdvleuten/addressing/commit/2d1b8fe8e7b501d8950a61349fa400bd1687d353))
14
+ * support validates_address_format on plain ActiveModel classes ([821469b](https://github.com/robinvdvleuten/addressing/commit/821469b20779854f8af78e84dcdf4ef1985c7580)), closes [#57](https://github.com/robinvdvleuten/addressing/issues/57)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * check origin_country with the other formatter options ([4e4dfbf](https://github.com/robinvdvleuten/addressing/commit/4e4dfbfe553bb106137b9e8b5076e7c1e5b8407a)), closes [#53](https://github.com/robinvdvleuten/addressing/issues/53)
20
+ * include address_line3 in validates_address_format default fields ([1739846](https://github.com/robinvdvleuten/addressing/commit/1739846ed3f5e3b03f3821fbb229f8a827078ed9)), closes [#47](https://github.com/robinvdvleuten/addressing/issues/47)
21
+ * load address formats from JSON and freeze their field lists ([e9bbbc1](https://github.com/robinvdvleuten/addressing/commit/e9bbbc1c8f5dd2c603fdc61676f27c9c238f7912)), closes [#43](https://github.com/robinvdvleuten/addressing/issues/43) [#49](https://github.com/robinvdvleuten/addressing/issues/49)
22
+ * make subdivision lookups independent of load order and platform ([31e1c4b](https://github.com/robinvdvleuten/addressing/commit/31e1c4b839419382d9cf95a66f51547906e65d89)), closes [#56](https://github.com/robinvdvleuten/addressing/issues/56)
23
+ * raise ArgumentError for a nil origin_country in PostalLabelFormatter ([954ba68](https://github.com/robinvdvleuten/addressing/commit/954ba68ce63224b5095e408160d762102b44752f)), closes [#45](https://github.com/robinvdvleuten/addressing/issues/45)
24
+ * read a subdivision definition that upstream writes as an empty array ([7dc6e72](https://github.com/robinvdvleuten/addressing/commit/7dc6e72fa76efe16e063f264a095bbdf83aafe1a))
25
+ * read the data files as UTF-8, independent of the locale ([aeafad8](https://github.com/robinvdvleuten/addressing/commit/aeafad861c637dd1e4b7e3b9e2f2931f129fa64c)), closes [#44](https://github.com/robinvdvleuten/addressing/issues/44)
26
+ * report a missing country code as a field violation ([15c23e6](https://github.com/robinvdvleuten/addressing/commit/15c23e63dc85a2cd394a2605fc57fd55ca8d0fb1)), closes [#58](https://github.com/robinvdvleuten/addressing/issues/58)
27
+ * share one blank check between Subdivision and AddressValidator ([b469c0b](https://github.com/robinvdvleuten/addressing/commit/b469c0b244bb771d624bebe9667e41090b46fba6))
28
+ * upcase country codes on Address and the postal label origin ([f1e6554](https://github.com/robinvdvleuten/addressing/commit/f1e6554d9291011f6e44bc91799646b9b7b39f62)), closes [#46](https://github.com/robinvdvleuten/addressing/issues/46)
29
+ * validate the html_tag and locale formatter options ([cbd90c3](https://github.com/robinvdvleuten/addressing/commit/cbd90c31daa569cd6418185bf4cfa36acc4d1078))
30
+
31
+
32
+ ### Performance Improvements
33
+
34
+ * cache fewer country lists and address formats ([1c5bc0a](https://github.com/robinvdvleuten/addressing/commit/1c5bc0a142ad70c8a54a11c97c9b9a0562a66ab8))
35
+ * check for subdivision children without building them ([04eb8c1](https://github.com/robinvdvleuten/addressing/commit/04eb8c135bfb66b088ca9bd26b174a0540574c0f))
36
+
37
+ ## [2.1.0](https://github.com/robinvdvleuten/addressing/compare/v2.0.1...v2.1.0) (2026-09-18)
38
+
39
+
40
+ ### Features
41
+
42
+ * sync data with commerceguys repository (v2.3.0) ([0ccf627](https://github.com/robinvdvleuten/addressing/commit/0ccf627b2abed8498d0b81bf57075346aa75c401))
43
+ * sync data with commerceguys repository (v2.3.1) ([acf6e04](https://github.com/robinvdvleuten/addressing/commit/acf6e042a3eb08f932f17db339689ab76baffacd))
44
+
5
45
  ## [2.0.1](https://github.com/robinvdvleuten/addressing/compare/v1.1.0...v2.0.1) (2026-05-27)
6
46
 
7
47
 
data/README.md CHANGED
@@ -1,146 +1,192 @@
1
1
  # Addressing
2
2
 
3
- A Ruby addressing library, powered by CLDR and Google's address data.
3
+ A Ruby library that knows how postal addresses are written in every country — which fields exist, in what order, what to call them, and which ones are required. Give it an address and a country code, and it produces a correctly formatted label or HTML block, validates the postal code, and tells your form whether to ask for a "State", a "Province", or a "Prefecture".
4
4
 
5
- - Countries, with translations for over 250 locales. Powered by [CLDR](http://cldr.unicode.org) v46.
6
- - Address formats for over 200 countries.
7
- - Subdivisions (administrative areas, localities, dependent localities) for 60 countries.
8
- - Both latin and local subdivision names, when relevant (e.g: Okinawa / 沖縄県).
9
- - Formatting, both in HTML and plain text.
5
+ Most country-data gems give you a list of countries and their subdivisions. This one gives you the **address format** behind each of them, so a checkout form or shipping label built on it is correct in Japan and Brazil, not just in the US.
10
6
 
11
- Address formats and subdivisions were initially generated from [Google's Address Data Service](https://chromium-i18n.appspot.com/ssl-address), and are now owned and maintained by the library itself.
7
+ - **Address formats for 205 countries.** Field order, required fields, uppercasing rules, postal code patterns, and the right label for each field.
8
+ - **256 countries, translated into 148 locales.** Names, three-letter and numeric codes, currency, and timezones. Powered by [CLDR](http://cldr.unicode.org) v48.
9
+ - **Subdivisions for 63 countries.** Up to three levels (administrative area → locality → dependent locality), in both latin and local scripts (Okinawa / 沖縄県).
10
+ - **Zero runtime dependencies.** Pure Ruby 3.3+. Address formats load from a single 56 KB JSON file; the 1.5 MB of country and subdivision data is read lazily, country names per locale and subdivisions per country, only when you ask for it.
11
+ - **Rails-ready.** A `validates_address_format` validator for Active Record and ActiveModel. On Active Record, it runs only when an address field actually changed.
12
+ - **Immutable.** `Address` objects never mutate; `with_*` methods return copies.
12
13
 
13
- [![Build Status](https://img.shields.io/github/actions/workflow/status/robinvdvleuten/addressing/test.yml?branch=main)](https://github.com/robinvdvleuten/addressing/actions?query=workflow%3Atest)
14
- [![MIT license](https://img.shields.io/github/license/robinvdvleuten/addressing.svg)](https://github.com/robinvdvleuten/addressing/blob/main/LICENSE)
14
+ ```rb
15
+ formatter = Addressing::DefaultFormatter.new(html: false)
16
+
17
+ puts formatter.format(Addressing::Address.new(country_code: "US",
18
+ administrative_area: "CA", locality: "Mountain View",
19
+ postal_code: "94043", address_line1: "1098 Alta Ave"))
20
+ # 1098 Alta Ave
21
+ # Mountain View, CA 94043
22
+ # United States
23
+
24
+ puts formatter.format(Addressing::Address.new(country_code: "JP",
25
+ administrative_area: "26", locality: "京都市南区",
26
+ postal_code: "601-8213", address_line1: "九条町1", locale: "ja"), locale: "ja")
27
+ # 日本
28
+ # 〒601-8213
29
+ # 京都府京都市南区
30
+ # 九条町1
31
+ ```
32
+
33
+ Same code, two countries, two completely different layouts — including the `〒` prefix and the reversed field order Japan uses.
34
+
35
+ Address formats and subdivisions were initially generated from [Google's Address Data Service](https://chromium-i18n.appspot.com/ssl-address), and are kept in sync with the PHP [commerceguys/addressing](https://github.com/commerceguys/addressing) library.
36
+
37
+ ---
38
+
39
+ ## Table of contents
40
+
41
+ - [Installation](#installation)
42
+ - [Addresses](#addresses)
43
+ - [Address formats](#address-formats)
44
+ - [Countries](#countries)
45
+ - [Subdivisions](#subdivisions)
46
+ - [Formatting addresses](#formatting-addresses)
47
+ - [Validating addresses](#validating-addresses)
48
+ - [Contributing](#contributing)
15
49
 
16
50
  ## Installation
17
51
 
18
- Add this line to your application’s Gemfile:
52
+ Add the gem to your Gemfile:
19
53
 
20
54
  ```rb
21
55
  gem "addressing"
22
56
  ```
23
57
 
24
- ## Getting Started
58
+ Then install it:
25
59
 
26
- The [Address](lib/addressing/address.rb) class represents a postal adddress, with attributes for the following fields:
60
+ ```
61
+ bundle install
62
+ ```
63
+
64
+ Requires Ruby 3.3 or newer. There are no other runtime dependencies — Active Record or ActiveModel is only needed for the validator, and `tzinfo` only for `Country#timezones`.
27
65
 
28
- - Country code
29
- - Administrative area
30
- - Locality (City)
31
- - Dependent Locality
32
- - Postal code
33
- - Sorting code
34
- - Address line 1
35
- - Address line 2
36
- - Address line 3
37
- - Organization
38
- - Given name (First name)
39
- - Additional name (Middle name / Patronymic)
40
- - Family name (Last name)
66
+ ## Addresses
41
67
 
42
- Field names follow the OASIS [eXtensible Address Language (xAL)](http://www.oasis-open.org/committees/ciq/download.shtml) standard.
68
+ The [Address](lib/addressing/address.rb) class represents a postal address. Field names follow the OASIS [eXtensible Address Language (xAL)](http://www.oasis-open.org/committees/ciq/download.shtml) standard:
69
+
70
+ `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`.
43
71
 
44
72
  ```rb
45
- # Create a new Address instance.
46
73
  address = Addressing::Address.new(
47
74
  country_code: "US",
48
75
  administrative_area: "CA",
49
76
  locality: "Mountain View",
50
- dependent_locality: "MV",
51
77
  postal_code: "94043",
52
- sorting_code: "94044",
53
78
  address_line1: "1600 Amphitheatre Parkway",
54
- address_line2: "Google Bldg 41",
55
79
  organization: "Google Inc.",
56
80
  given_name: "John",
57
- additional_name: "L.",
58
- family_name: "Smith",
59
- locale: "en"
81
+ family_name: "Smith"
60
82
  )
83
+ ```
61
84
 
62
- # Modify an existing instance through chainable methods.
63
- address = address.with_country_code('US')
64
- .with_administrative_area('CA')
85
+ Addresses are immutable. The `with_*` methods return a modified copy and can be chained:
86
+
87
+ ```rb
88
+ address = Addressing::Address.new
89
+ .with_country_code("US")
90
+ .with_administrative_area("CA")
91
+ .with_locality("Mountain View")
92
+ .with_address_line1("1098 Alta Ave")
65
93
  ```
66
94
 
67
- The [AddressFormat](lib/addressing/address_format.rb) class provides the following information:
95
+ ## Address formats
68
96
 
69
- - Which fields are used, and in which order
70
- - Which fields are required
71
- - Which fields need to be uppercased for the actual mailing (to facilitate automated sorting of mail)
72
- - The labels for the administrative area (state, province, parish, etc.), locality (city/post town/district, etc.), dependent locality (neighborhood, suburb, district, etc) and the postal code (postal code or ZIP code)
73
- - The regular expression pattern for validating postal codes
97
+ The [AddressFormat](lib/addressing/address_format.rb) class describes how a country writes its addresses: which fields are used and in which order, which are required, which must be uppercased for mailing, what each field is called, the postal code pattern, and which fields have predefined subdivision data.
74
98
 
75
99
  ```rb
76
- # Get the address format for Brazil.
77
- address_format = Addressing::AddressFormat.get('BR')
100
+ format = Addressing::AddressFormat.get("BR")
101
+
102
+ p format.required_fields
103
+ # ["address_line1", "administrative_area", "locality", "postal_code", "given_name", "family_name"]
104
+
105
+ p format.administrative_area_type # "state"
106
+ p format.postal_code_type # "postal"
107
+ p format.subdivision_fields # ["administrative_area", "locality"]
78
108
  ```
79
109
 
80
- The [Country](lib/addressing/country.rb) class provides the following information:
110
+ Use `administrative_area_type`, `locality_type`, `dependent_locality_type`, and `postal_code_type` to label your form fields the way locals expect — "state" in Brazil, "prefecture" in Japan, "county" in Ireland.
111
+
112
+ ## Countries
81
113
 
82
- - The country name.
83
- - The numeric and three-letter country codes.
84
- - The official currency code, when known.
85
- - The timezones which the country spans.
114
+ The [Country](lib/addressing/country.rb) class provides the country name, the numeric and three-letter codes, the official currency code when known, and the timezones the country spans.
86
115
 
87
116
  ```rb
88
- # Get the country instance for Brazil.
89
- brazil = Addressing::Country.get('BR')
90
- p brazil.three_letter_code # BRA
91
- p brazil.name # Brazil
92
- p brazil.currency_code # BRL
93
- p brazil.timezones
117
+ brazil = Addressing::Country.get("BR")
118
+
119
+ p brazil.three_letter_code # "BRA"
120
+ p brazil.name # "Brazil"
121
+ p brazil.currency_code # "BRL"
94
122
 
95
- # Get all country instances.
123
+ # Get all countries as a hash of country_code => Country.
96
124
  countries = Addressing::Country.all
97
125
 
98
- # Get the country list ({ country_code => name }), in French.
99
- country_list = Addressing::Country.list('fr-FR')
126
+ # Get a { country_code => name } list, in French — useful for a <select>.
127
+ p Addressing::Country.list("fr-FR")["BR"] # "Brésil"
128
+
129
+ # Or a single country in another locale.
130
+ p Addressing::Country.get("BR", "fr-FR").name # "Brésil"
131
+ ```
132
+
133
+ > `Country#timezones` is backed by the [tzinfo](https://github.com/tzinfo/tzinfo) gem. Add `gem "tzinfo"` (and `tzinfo-data` on platforms without a system timezone database) if you use it.
134
+
135
+ ```rb
136
+ require "tzinfo"
137
+ p brazil.timezones.first(2) # ["America/Noronha", "America/Belem"]
100
138
  ```
101
139
 
102
- The [Subdivision](lib/addressing/subdivision.rb) class provides the following information:
140
+ ## Subdivisions
103
141
 
104
- - The subdivision code (used to represent the subdivison on a parcel/envelope, e.g. CA for California)
105
- - The subdivison name (shown to the user in a dropdown)
106
- - The local code and name, if the country uses a non-latin script (e.g. Cyrilic in Russia).
107
- - The postal code pattern (if different from the one on the address format).
142
+ The [Subdivision](lib/addressing/subdivision.rb) class provides the subdivision code used on an envelope (`CA` for California), the name shown to the user, the local code and name for countries using a non-latin script, and a postal code pattern when it differs from the country's.
108
143
 
109
- Subdivisions are hierarchical and can have up to three levels: Administrative Area -> Locality -> Dependent Locality.
144
+ Subdivisions are hierarchical, up to three levels: administrative area → locality → dependent locality. Pass the parents as an array.
110
145
 
111
146
  ```rb
112
- # Get the subdivisions for Brazil.
113
- states = Addressing::Subdivision.all(['BR'])
114
- states.each do |state|
115
- municipalities = state.children
116
- end
147
+ # All Brazilian states.
148
+ states = Addressing::Subdivision.all(["BR"])
149
+ p states.size # 27
117
150
 
118
- # Get the subdivisions for Brazilian state Ceará.
119
- municipalities = Addressing::Subdivision.all(['BR', 'CE'])
120
- municipalities.each do |municipality|
121
- p municipality.name
122
- end
151
+ # One state, by code.
152
+ ceara = Addressing::Subdivision.get("CE", ["BR"])
153
+ p [ceara.code, ceara.name, ceara.children?] # ["CE", "Ceará", true]
154
+
155
+ # The municipalities of Ceará.
156
+ p Addressing::Subdivision.all(["BR", "CE"]).size # 184
157
+
158
+ # A { code => name } list, ready for a <select>.
159
+ Addressing::Subdivision.list(["BR"])
123
160
  ```
124
161
 
125
- ### Formatting addresses
162
+ Data is loaded lazily: asking for Brazil's states reads Brazil's file only, and `children` on a subdivision loads that branch on first access. `children` behaves like the hash that `all` returns.
126
163
 
127
- Addresses are formatted according to the address format, in HTML or text.
164
+ To match the values of an address against the predefined subdivisions, ask for its subdivision chain. Pass the values in level order: administrative area, locality, dependent locality. The walk stops at an empty value, at a subdivision without children, or at a value that matches no predefined subdivision. `unmatched_level` gives the position of that last value.
128
165
 
129
- #### DefaultFormatter
166
+ ```rb
167
+ chain = Addressing::Subdivision.chain("BR", ["CE", "Fortaleza"])
168
+ p chain.subdivisions.map(&:name) # ["Ceará", "Fortaleza"]
169
+ p chain.unmatched_level # nil
170
+
171
+ p Addressing::Subdivision.chain("BR", ["CE", "Nowhere"]).unmatched_level # 1
172
+ ```
130
173
 
131
- Formats an address for display, always adds the localized country name.
174
+ ## Formatting addresses
175
+
176
+ Both formatters render according to the country's address format, in HTML (the default) or plain text (`html: false`).
177
+
178
+ ### DefaultFormatter
179
+
180
+ Formats an address for display, always adding the localized country name.
132
181
 
133
182
  ```rb
134
183
  address = Addressing::Address.new
135
- address = address.with_country_code('US')
136
- .with_administrative_area('CA')
137
- .with_locality('Mountain View')
138
- .with_address_line1('1098 Alta Ave')
139
-
140
- formatter = Addressing::DefaultFormatter.new
141
- p formatter.format(address)
184
+ .with_country_code("US")
185
+ .with_administrative_area("CA")
186
+ .with_locality("Mountain View")
187
+ .with_address_line1("1098 Alta Ave")
142
188
 
143
- # Output:
189
+ puts Addressing::DefaultFormatter.new.format(address)
144
190
  # <p translate="no">
145
191
  # <span class="address-line1">1098 Alta Ave</span><br>
146
192
  # <span class="locality">Mountain View</span>, <span class="administrative-area">CA</span><br>
@@ -148,34 +194,29 @@ p formatter.format(address)
148
194
  # </p>
149
195
  ```
150
196
 
151
- #### PostalLabelFormatter
197
+ ### PostalLabelFormatter
152
198
 
153
- Takes care of uppercasing fields where required by the format (to facilitate automated mail sorting).
199
+ Renders a mailing label as plain text, uppercasing the fields the country requires for automated mail sorting.
154
200
 
155
- Requires specifying the origin country code, allowing it to differentiate between domestic and international mail. In case of domestic mail, the country name is not displayed at all. In case of international mail:
156
-
157
- 1. The postal code is prefixed with the destination's postal code prefix.
158
- 2. The country name is added to the formatted address, in both the current locale and English. This matches the recommendation given by the Universal Postal Union, to avoid difficulties in countries of transit.
201
+ It needs the origin country, so it can tell domestic mail from international. For domestic mail the country name is omitted entirely. For international mail the postal code gets the destination's prefix, and the country name is added in both the current locale and English — the Universal Postal Union's recommendation, to avoid trouble in countries of transit.
159
202
 
160
203
  ```rb
161
204
  address = Addressing::Address.new
162
- address = address.with_country_code('US')
163
- .with_administrative_area('CA')
164
- .with_locality('Mountain View')
165
- .with_address_line1('1098 Alta Ave')
166
-
167
- formatter = Addressing::PostalLabelFormatter.new
168
- p formatter.format(address, origin_country: "FR")
205
+ .with_country_code("US")
206
+ .with_administrative_area("CA")
207
+ .with_locality("Mountain View")
208
+ .with_postal_code("94043")
209
+ .with_address_line1("1098 Alta Ave")
169
210
 
170
- # Output:
211
+ puts Addressing::PostalLabelFormatter.new.format(address, origin_country: "FR", locale: "fr-FR")
171
212
  # 1098 Alta Ave
172
213
  # MOUNTAIN VIEW, CA 94043
173
214
  # ÉTATS-UNIS - UNITED STATES
174
215
  ```
175
216
 
176
- ### Validating addresses
217
+ ## Validating addresses
177
218
 
178
- For Active Record models, use:
219
+ For Active Record models:
179
220
 
180
221
  ```rb
181
222
  class User < ApplicationRecord
@@ -183,32 +224,91 @@ class User < ApplicationRecord
183
224
  end
184
225
  ```
185
226
 
186
- For performance, the address is only verified if at least one of the fields changes. Set your own condition with:
227
+ For any other ActiveModel class, extend `Addressing::Model`:
228
+
229
+ ```rb
230
+ class ShippingAddress
231
+ include ActiveModel::Model
232
+ extend Addressing::Model
233
+
234
+ attr_accessor :country_code, :administrative_area, :locality, :postal_code, :address_line1
235
+
236
+ validates_address_format
237
+ end
238
+ ```
239
+
240
+ This checks that every field the country requires is present, that no unused field is filled in, that the subdivisions exist, and that the postal code matches the country's pattern.
241
+
242
+ By default it validates all address fields. On a model that tracks changes, such as an Active Record model, it only runs when at least one of them has changed; on a model without change tracking, it runs on every validation. Pass `fields:` to narrow it down:
187
243
 
188
244
  ```rb
189
245
  class User < ApplicationRecord
190
- validates_address if: -> { something_changed? }, ...
246
+ validates_address_format fields: [:country_code, :administrative_area, :locality, :postal_code, :address_line1]
191
247
  end
192
248
  ```
193
249
 
194
- ## Changelog
250
+ > **Note:** `fields:` controls which attributes are *read from your model*, not which ones the country requires. The US format requires a given name and family name, so a model without those columns will fail validation with "should not be blank". Use `field_overrides:` to tell the validator that your application does not collect them. Always include `:country_code`: without it the address has no country, and validation reports `country_code` as blank.
195
251
 
196
- Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
252
+ ```rb
253
+ class User < ApplicationRecord
254
+ validates_address_format(
255
+ fields: [:country_code, :administrative_area, :locality, :postal_code, :address_line1],
256
+ field_overrides: Addressing::FieldOverrides.new(
257
+ Addressing::AddressField::GIVEN_NAME => Addressing::FieldOverride::HIDDEN,
258
+ Addressing::AddressField::FAMILY_NAME => Addressing::FieldOverride::HIDDEN,
259
+ Addressing::AddressField::ORGANIZATION => Addressing::FieldOverride::HIDDEN
260
+ )
261
+ )
262
+ end
263
+ ```
197
264
 
198
- ## Acknowledgements
265
+ ```rb
266
+ User.new(country_code: "US", administrative_area: "CA", locality: "Mountain View",
267
+ postal_code: "94043", address_line1: "1098 Alta Ave").valid?
268
+ # => true
269
+
270
+ user = User.new(country_code: "US", administrative_area: "XX", locality: "Mountain View",
271
+ postal_code: "9404", address_line1: "1098 Alta Ave")
272
+ user.valid?
273
+ # => false
274
+ user.errors.full_messages
275
+ # => ["Administrative area should be valid", "Postal code should be valid"]
276
+ ```
199
277
 
200
- This gem wouldn't exist when there wasn't the awesome PHP [addressing](https://github.com/commerceguys/addressing) library. The [CommerceGuys](https://github.com/commerceguys) did an excellent job figuring out how to parse Google's address data, as described by their [backstory](https://drupalcommerce.org/blog/16864/commerce-2x-stories-addressing). Unfortunately for me, they created a PHP library where I needed a Ruby gem so this project was born.
278
+ Each field can be overridden as `HIDDEN`, `OPTIONAL`, or `REQUIRED`. Skip postal code checking with `verify_postal_code: false`, and replace the default change-detection with any validation option, such as `if:` or `unless:`:
279
+
280
+ ```rb
281
+ validates_address_format if: -> { shipping_address_changed? }
282
+ ```
283
+
284
+ ### Validating without a model
285
+
286
+ [AddressValidator](lib/addressing/address_validator.rb) applies the same rules to a plain `Addressing::Address`, and takes the same `field_overrides:` and `verify_postal_code:` options. It returns a list of field violations; an empty list means the address is valid.
287
+
288
+ ```rb
289
+ address = Addressing::Address.new(country_code: "US", administrative_area: "XX", locality: "Mountain View",
290
+ postal_code: "9404", address_line1: "1098 Alta Ave",
291
+ given_name: "John", family_name: "Smith")
292
+
293
+ Addressing::AddressValidator.validate(address)
294
+ # => [#<data Addressing::FieldViolation field=:administrative_area, kind=:invalid>,
295
+ # #<data Addressing::FieldViolation field=:postal_code, kind=:invalid>]
296
+ ```
297
+
298
+ Each [FieldViolation](lib/addressing/field_violation.rb) holds the `field` as a symbol and the `kind` of rule it breaks: `:blank` for a required field that is missing, `:present` for a field the country does not use or that is hidden, and `:invalid` for a subdivision or postal code that does not match. The kinds are the error types that `validates_address_format` adds to `errors.details`.
299
+
300
+ An address without a country code gives a single `:blank` violation on `country_code`, because there is no address format to check the other fields against. If your model already validates the presence of `country_code`, you can drop that validation.
201
301
 
202
302
  ## Contributing
203
303
 
204
- Everyone is encouraged to help improve this project. Here are a few ways you can help:
304
+ Everyone is encouraged to help improve this project:
205
305
 
206
306
  - [Report bugs](https://github.com/robinvdvleuten/addressing/issues)
207
307
  - Fix bugs and [submit pull requests](https://github.com/robinvdvleuten/addressing/pulls)
208
308
  - Write, clarify, or fix documentation
209
309
  - Suggest or add new features
210
310
 
211
- To get started with development:
311
+ To get started:
212
312
 
213
313
  ```
214
314
  git clone https://github.com/robinvdvleuten/addressing.git
@@ -217,8 +317,20 @@ bundle install
217
317
  bundle exec rake test
218
318
  ```
219
319
 
320
+ Refreshing the country and address data from upstream (`rake addressing:generate`) additionally requires PHP, since it reads the definitions out of the commerceguys library. It syncs the version pinned in `tasks/data_sync.rb`, or another tag with `rake "addressing:generate[v2.3.2]"`, and writes only to `data/`. `data/UPSTREAM_VERSION` records which tag the data comes from.
321
+
322
+ Upstream maintains the subdivisions and address formats by hand, so the data files can disagree with each other. `rake addressing:verify` reports such discrepancies, and runs at the end of every refresh and in the Verify data workflow. Report a discrepancy upstream instead of editing the data files here, and add it to `tasks/known_discrepancies.yml` until the fix arrives.
323
+
220
324
  Feel free to open an issue to get feedback on your idea before spending too much time on it.
221
325
 
326
+ ## Changelog
327
+
328
+ See [CHANGELOG.md](CHANGELOG.md) for what has changed recently.
329
+
330
+ ## Acknowledgements
331
+
332
+ This gem wouldn't exist without the PHP [addressing](https://github.com/commerceguys/addressing) library. The [CommerceGuys](https://github.com/commerceguys) did an excellent job figuring out how to parse Google's address data, as described in their [backstory](https://drupalcommerce.org/blog/16864/commerce-2x-stories-addressing). They built a PHP library where I needed a Ruby gem, so this project was born.
333
+
222
334
  ## License
223
335
 
224
- The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
336
+ The MIT License (MIT). See [LICENSE](LICENSE) for more information.
@@ -0,0 +1 @@
1
+ v2.3.1