addressing 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/addressing/address.rb +66 -78
- data/lib/addressing/address_format.rb +19 -8
- data/lib/addressing/country.rb +41 -11
- data/lib/addressing/default_formatter.rb +33 -19
- data/lib/addressing/enum.rb +4 -5
- data/lib/addressing/exceptions.rb +29 -0
- data/lib/addressing/field_override.rb +6 -13
- data/lib/addressing/locale.rb +1 -1
- data/lib/addressing/model.rb +0 -6
- data/lib/addressing/postal_label_formatter.rb +1 -1
- data/lib/addressing/subdivision.rb +36 -22
- data/lib/addressing/version.rb +1 -1
- data/lib/addressing.rb +1 -8
- metadata +4 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25d057964fdb1ec9c73f9652e2ce82974abf5309b52047e0927a51d51aed78da
|
4
|
+
data.tar.gz: 2c6e90b8c9db02dfb442e65a2bd11d5e6fa690f9e9772d30545a9feba6c9a9c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cd5595a0644d74eafcb7e859576ab24e9aaf4e684981cbe5d4ec9039fd425cb5659c6c33c5b9111f22c7e9ba6d8cc5d46821341c4124f22670c15cb54e4df69
|
7
|
+
data.tar.gz: 844a1e91ec57d11b867aa8c9d381661e6e560bc5d4f1b797bf363eb66528fbf345f9ff324214a1bcafd50b125aa789c5961bda732e7d0c056fd5e4fe7738700e
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,23 @@
|
|
2
2
|
|
3
3
|
All notable changes to `addressing` will be documented in this file.
|
4
4
|
|
5
|
+
## 1.1.0 (2025-10-03)
|
6
|
+
|
7
|
+
- Add formatter-level caching for Country.list
|
8
|
+
- Refactor FieldOverrides initialization to be more idiomatic
|
9
|
+
- Add equality methods to Address class
|
10
|
+
- Refactor complex values method in DefaultFormatter
|
11
|
+
- Extract magic strings to constants
|
12
|
+
- Simplify boolean validation in formatter options
|
13
|
+
- use .new() for UnknownLocaleError
|
14
|
+
- Added RDoc documentation
|
15
|
+
- Replaced class variables with class instance variables
|
16
|
+
- Add custom exception classes for better error handling
|
17
|
+
- Refactor Address class
|
18
|
+
- Fix test bug in address_test.rb
|
19
|
+
- Remove duplicate code in Model validation
|
20
|
+
- add missing % to generic address format
|
21
|
+
|
5
22
|
## 1.0.0 (2024-10-21)
|
6
23
|
|
7
24
|
- Sync data with commerceguys repository (v2.2.2)
|
data/lib/addressing/address.rb
CHANGED
@@ -1,9 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Addressing
|
4
|
+
# Represents a postal address with attributes for country, administrative areas,
|
5
|
+
# postal code, address lines, and recipient information.
|
6
|
+
#
|
7
|
+
# Address objects are immutable - use the +with_*+ methods to create modified copies.
|
8
|
+
#
|
9
|
+
# @example Creating an address
|
10
|
+
# address = Addressing::Address.new(
|
11
|
+
# country_code: "US",
|
12
|
+
# administrative_area: "CA",
|
13
|
+
# locality: "Mountain View",
|
14
|
+
# postal_code: "94043",
|
15
|
+
# address_line1: "1600 Amphitheatre Parkway"
|
16
|
+
# )
|
17
|
+
#
|
18
|
+
# @example Modifying an address
|
19
|
+
# updated = address.with_postal_code("94044")
|
4
20
|
class Address
|
5
|
-
|
6
|
-
|
21
|
+
FIELDS = %i[
|
22
|
+
country_code administrative_area locality dependent_locality
|
23
|
+
postal_code sorting_code address_line1 address_line2 address_line3
|
24
|
+
organization given_name additional_name family_name locale
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
attr_reader(*FIELDS)
|
28
|
+
|
29
|
+
# Creates a new Address instance.
|
30
|
+
#
|
31
|
+
# @param country_code [String] ISO 3166-1 alpha-2 country code
|
32
|
+
# @param administrative_area [String] Top-level administrative subdivision (state, province, etc.)
|
33
|
+
# @param locality [String] City or locality
|
34
|
+
# @param dependent_locality [String] Dependent locality (neighborhood, suburb, district, etc.)
|
35
|
+
# @param postal_code [String] Postal code
|
36
|
+
# @param sorting_code [String] Sorting code (used in some countries)
|
37
|
+
# @param address_line1 [String] First line of the street address
|
38
|
+
# @param address_line2 [String] Second line of the street address
|
39
|
+
# @param address_line3 [String] Third line of the street address
|
40
|
+
# @param organization [String] Organization name
|
41
|
+
# @param given_name [String] Given name (first name)
|
42
|
+
# @param additional_name [String] Additional name (middle name, patronymic)
|
43
|
+
# @param family_name [String] Family name (last name)
|
44
|
+
# @param locale [String] Locale code for the address
|
7
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")
|
8
46
|
@country_code = country_code
|
9
47
|
@administrative_area = administrative_area
|
@@ -21,92 +59,42 @@ module Addressing
|
|
21
59
|
@locale = locale
|
22
60
|
end
|
23
61
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
address = clone
|
32
|
-
address.administrative_area = administrative_area
|
33
|
-
address
|
34
|
-
end
|
35
|
-
|
36
|
-
def with_locality(locality)
|
37
|
-
address = clone
|
38
|
-
address.locality = locality
|
39
|
-
address
|
40
|
-
end
|
41
|
-
|
42
|
-
def with_dependent_locality(dependent_locality)
|
43
|
-
address = clone
|
44
|
-
address.dependent_locality = dependent_locality
|
45
|
-
address
|
46
|
-
end
|
47
|
-
|
48
|
-
def with_postal_code(postal_code)
|
49
|
-
address = clone
|
50
|
-
address.postal_code = postal_code
|
51
|
-
address
|
52
|
-
end
|
53
|
-
|
54
|
-
def with_sorting_code(sorting_code)
|
55
|
-
address = clone
|
56
|
-
address.sorting_code = sorting_code
|
57
|
-
address
|
58
|
-
end
|
59
|
-
|
60
|
-
def with_address_line1(address_line1)
|
61
|
-
address = clone
|
62
|
-
address.address_line1 = address_line1
|
63
|
-
address
|
64
|
-
end
|
65
|
-
|
66
|
-
def with_address_line2(address_line2)
|
67
|
-
address = clone
|
68
|
-
address.address_line2 = address_line2
|
69
|
-
address
|
62
|
+
# Define with_* methods for all fields
|
63
|
+
FIELDS.each do |field|
|
64
|
+
define_method(:"with_#{field}") do |value|
|
65
|
+
address = clone
|
66
|
+
address.send(:"#{field}=", value)
|
67
|
+
address
|
68
|
+
end
|
70
69
|
end
|
71
70
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
def with_organization(organization)
|
79
|
-
address = clone
|
80
|
-
address.organization = organization
|
81
|
-
address
|
82
|
-
end
|
83
|
-
|
84
|
-
def with_given_name(given_name)
|
85
|
-
address = clone
|
86
|
-
address.given_name = given_name
|
87
|
-
address
|
88
|
-
end
|
71
|
+
# Compares two addresses for equality based on all field values.
|
72
|
+
#
|
73
|
+
# @param other [Object] The object to compare with
|
74
|
+
# @return [Boolean] true if all fields are equal
|
75
|
+
def ==(other)
|
76
|
+
return false unless other.is_a?(Address)
|
89
77
|
|
90
|
-
|
91
|
-
address = clone
|
92
|
-
address.additional_name = additional_name
|
93
|
-
address
|
78
|
+
FIELDS.all? { |field| send(field) == other.send(field) }
|
94
79
|
end
|
95
80
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
81
|
+
# Compares two addresses for equality (alias for ==).
|
82
|
+
#
|
83
|
+
# @param other [Object] The object to compare with
|
84
|
+
# @return [Boolean] true if all fields are equal
|
85
|
+
def eql?(other)
|
86
|
+
self == other
|
100
87
|
end
|
101
88
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
89
|
+
# Generates a hash code for the address based on all field values.
|
90
|
+
#
|
91
|
+
# @return [Integer] hash code
|
92
|
+
def hash
|
93
|
+
FIELDS.map { |field| send(field) }.hash
|
106
94
|
end
|
107
95
|
|
108
96
|
protected
|
109
97
|
|
110
|
-
attr_writer
|
98
|
+
attr_writer(*FIELDS)
|
111
99
|
end
|
112
100
|
end
|
@@ -1,20 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Addressing
|
4
|
+
# Provides address format information for countries.
|
5
|
+
#
|
6
|
+
# Address formats define which fields are used, their order, requirements,
|
7
|
+
# and formatting rules for postal addresses in different countries.
|
8
|
+
#
|
9
|
+
# @example Get address format for Brazil
|
10
|
+
# format = Addressing::AddressFormat.get('BR')
|
11
|
+
# format.used_fields # => ["given_name", "family_name", ...]
|
12
|
+
# format.required_fields # => ["address_line1", "locality", ...]
|
4
13
|
class AddressFormat
|
5
14
|
class << self
|
6
|
-
#
|
7
|
-
|
8
|
-
|
15
|
+
# Gets the address format for the provided country code.
|
16
|
+
#
|
17
|
+
# @param country_code [String] ISO 3166-1 alpha-2 country code
|
18
|
+
# @return [AddressFormat] Address format instance
|
9
19
|
def get(country_code)
|
10
20
|
country_code = country_code.upcase
|
21
|
+
@address_formats ||= {}
|
11
22
|
|
12
|
-
unless
|
23
|
+
unless @address_formats.key?(country_code)
|
13
24
|
definition = process_definition(definitions[country_code] || {country_code: country_code})
|
14
|
-
|
25
|
+
@address_formats[country_code] = new(definition)
|
15
26
|
end
|
16
27
|
|
17
|
-
|
28
|
+
@address_formats[country_code]
|
18
29
|
end
|
19
30
|
|
20
31
|
def all
|
@@ -27,7 +38,7 @@ module Addressing
|
|
27
38
|
private
|
28
39
|
|
29
40
|
def definitions
|
30
|
-
|
41
|
+
@definitions ||= Marshal.load(File.read(File.expand_path("../../../data/address_formats.dump", __FILE__).to_s))
|
31
42
|
end
|
32
43
|
|
33
44
|
def process_definition(definition)
|
@@ -42,7 +53,7 @@ module Addressing
|
|
42
53
|
|
43
54
|
def generic_definition
|
44
55
|
{
|
45
|
-
format: "%given_name %family_name\n%organization\n%address_line1\n%address_line2\n
|
56
|
+
format: "%given_name %family_name\n%organization\n%address_line1\n%address_line2\n%address_line3\n%locality",
|
46
57
|
required_fields: [
|
47
58
|
"address_line1", "locality"
|
48
59
|
],
|
data/lib/addressing/country.rb
CHANGED
@@ -1,11 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Addressing
|
4
|
+
# Provides country information including names, codes, currency, and timezones.
|
5
|
+
#
|
6
|
+
# Country names are available in over 250 locales powered by CLDR data.
|
7
|
+
#
|
8
|
+
# @example Get a country by code
|
9
|
+
# brazil = Addressing::Country.get('BR')
|
10
|
+
# brazil.name # => "Brazil"
|
11
|
+
# brazil.currency_code # => "BRL"
|
12
|
+
#
|
13
|
+
# @example Get all countries
|
14
|
+
# countries = Addressing::Country.all('fr-FR')
|
15
|
+
#
|
16
|
+
# @example Get a simple list of countries
|
17
|
+
# list = Addressing::Country.list('en')
|
4
18
|
class Country
|
5
19
|
class << self
|
6
|
-
|
7
|
-
|
8
|
-
@@available_locales = [
|
20
|
+
AVAILABLE_LOCALES = [
|
9
21
|
"af", "am", "ar", "ar-LY", "ar-SA", "as", "az", "be", "bg", "bn",
|
10
22
|
"bn-IN", "bs", "ca", "chr", "cs", "cy", "da", "de", "de-AT", "de-CH",
|
11
23
|
"dsb", "el", "el-polyton", "en", "en-001", "en-AU", "en-CA", "en-ID",
|
@@ -23,12 +35,19 @@ module Addressing
|
|
23
35
|
"uz", "vi", "yue", "yue-Hans", "zh", "zh-Hant", "zh-Hant-HK", "zu"
|
24
36
|
]
|
25
37
|
|
38
|
+
# Gets a Country instance for the provided country code.
|
39
|
+
#
|
40
|
+
# @param country_code [String] ISO 3166-1 alpha-2 country code
|
41
|
+
# @param locale [String] Locale for the country name (default: "en")
|
42
|
+
# @param fallback_locale [String] Fallback locale if requested locale is unavailable
|
43
|
+
# @return [Country] Country instance
|
44
|
+
# @raise [UnknownCountryError] if the country code is not recognized
|
26
45
|
def get(country_code, locale = "en", fallback_locale = "en")
|
27
46
|
country_code = country_code.upcase
|
28
47
|
|
29
|
-
raise UnknownCountryError
|
48
|
+
raise UnknownCountryError.new(country_code) unless base_definitions.key?(country_code)
|
30
49
|
|
31
|
-
locale = Locale.resolve(
|
50
|
+
locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
|
32
51
|
definitions = load_definitions(locale)
|
33
52
|
|
34
53
|
new(
|
@@ -41,8 +60,13 @@ module Addressing
|
|
41
60
|
)
|
42
61
|
end
|
43
62
|
|
63
|
+
# Gets all Country instances.
|
64
|
+
#
|
65
|
+
# @param locale [String] Locale for country names (default: "en")
|
66
|
+
# @param fallback_locale [String] Fallback locale if requested locale is unavailable
|
67
|
+
# @return [Hash<String, Country>] Hash of country code => Country instance
|
44
68
|
def all(locale = "en", fallback_locale = "en")
|
45
|
-
locale = Locale.resolve(
|
69
|
+
locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
|
46
70
|
definitions = load_definitions(locale)
|
47
71
|
|
48
72
|
definitions.map do |country_code, country_name|
|
@@ -59,8 +83,13 @@ module Addressing
|
|
59
83
|
end.to_h
|
60
84
|
end
|
61
85
|
|
86
|
+
# Gets a list of country codes and names.
|
87
|
+
#
|
88
|
+
# @param locale [String] Locale for country names (default: "en")
|
89
|
+
# @param fallback_locale [String] Fallback locale if requested locale is unavailable
|
90
|
+
# @return [Hash<String, String>] Hash of country code => country name
|
62
91
|
def list(locale = "en", fallback_locale = "en")
|
63
|
-
locale = Locale.resolve(
|
92
|
+
locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
|
64
93
|
definitions = load_definitions(locale)
|
65
94
|
|
66
95
|
definitions.map do |country_code, country_name|
|
@@ -72,19 +101,20 @@ module Addressing
|
|
72
101
|
|
73
102
|
# Loads the country definitions for the provided locale.
|
74
103
|
def load_definitions(locale)
|
75
|
-
|
104
|
+
@definitions ||= {}
|
105
|
+
unless @definitions.key?(locale)
|
76
106
|
filename = File.join(File.expand_path("../../../data/country", __FILE__).to_s, "#{locale}.json")
|
77
|
-
|
107
|
+
@definitions[locale] = JSON.parse(File.read(filename))
|
78
108
|
end
|
79
109
|
|
80
|
-
|
110
|
+
@definitions[locale]
|
81
111
|
end
|
82
112
|
|
83
113
|
# Gets the base country definitions.
|
84
114
|
#
|
85
115
|
# Contains data common to all locales: three letter code, numeric code.
|
86
116
|
def base_definitions
|
87
|
-
|
117
|
+
@base_definitions ||= {
|
88
118
|
"AC" => ["ASC", nil, "SHP"],
|
89
119
|
"AD" => ["AND", "020", "EUR"],
|
90
120
|
"AE" => ["ARE", "784", "AED"],
|
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
module Addressing
|
4
4
|
class DefaultFormatter
|
5
|
+
DEFAULT_LOCALE = "en"
|
6
|
+
FORMAT_PLACEHOLDER_PATTERN = /%[a-z1-9_]+/
|
7
|
+
LEADING_TRAILING_PUNCTUATION_PATTERN = /\A[ \-,]+|[ \-,]+\z/
|
8
|
+
MULTIPLE_SPACES_PATTERN = /\s\s+/
|
9
|
+
|
5
10
|
DEFAULT_OPTIONS = {
|
6
|
-
locale:
|
11
|
+
locale: DEFAULT_LOCALE,
|
7
12
|
html: true,
|
8
13
|
html_tag: "p",
|
9
14
|
html_attributes: {translate: "no"}
|
@@ -13,6 +18,7 @@ module Addressing
|
|
13
18
|
assert_options(default_options)
|
14
19
|
|
15
20
|
@default_options = self.class::DEFAULT_OPTIONS.merge(default_options)
|
21
|
+
@country_list_cache = {}
|
16
22
|
end
|
17
23
|
|
18
24
|
def format(address, options = {})
|
@@ -33,7 +39,7 @@ module Addressing
|
|
33
39
|
view = render_view(view)
|
34
40
|
|
35
41
|
replacements = view.map { |key, element| ["%#{key}", element] }.to_h
|
36
|
-
output = format_string.gsub(
|
42
|
+
output = format_string.gsub(FORMAT_PLACEHOLDER_PATTERN) { |m| replacements[m] }
|
37
43
|
output = clean_output(output)
|
38
44
|
|
39
45
|
if options[:html]
|
@@ -49,7 +55,7 @@ module Addressing
|
|
49
55
|
|
50
56
|
# Builds the view for the given address.
|
51
57
|
def build_view(address, address_format, options)
|
52
|
-
countries =
|
58
|
+
countries = country_list(options[:locale])
|
53
59
|
values = values(address, address_format).merge({"country" => countries.key?(address.country_code) ? countries[address.country_code] : address.country_code})
|
54
60
|
used_fields = address_format.used_fields + ["country"]
|
55
61
|
|
@@ -58,6 +64,11 @@ module Addressing
|
|
58
64
|
end.to_h
|
59
65
|
end
|
60
66
|
|
67
|
+
# Gets the country list for a locale, with caching.
|
68
|
+
def country_list(locale)
|
69
|
+
@country_list_cache[locale] ||= Country.list(locale)
|
70
|
+
end
|
71
|
+
|
61
72
|
# Renders the given view.
|
62
73
|
def render_view(view)
|
63
74
|
view.map do |key, element|
|
@@ -90,27 +101,34 @@ module Addressing
|
|
90
101
|
|
91
102
|
# Removes empty lines, leading punctuation, excess whitespace.
|
92
103
|
def clean_output(output)
|
93
|
-
output.split("\n").map { |line| line.gsub(
|
104
|
+
output.split("\n").map { |line| line.gsub(LEADING_TRAILING_PUNCTUATION_PATTERN, "").strip.gsub(MULTIPLE_SPACES_PATTERN, " ") }.reject(&:empty?).join("\n")
|
94
105
|
end
|
95
106
|
|
96
107
|
# Gets the address values used to build the view.
|
97
108
|
def values(address, address_format)
|
98
|
-
values =
|
109
|
+
values = extract_address_values(address)
|
110
|
+
resolve_subdivision_values(values, address, address_format)
|
111
|
+
values
|
112
|
+
end
|
113
|
+
|
114
|
+
# Extracts all address field values.
|
115
|
+
def extract_address_values(address)
|
116
|
+
AddressField.all.map { |_, field| [field, address.send(field)] }.to_h
|
117
|
+
end
|
118
|
+
|
119
|
+
# Resolves subdivision values to their display codes.
|
120
|
+
def resolve_subdivision_values(values, address, address_format)
|
99
121
|
subdivision_fields = address_format.used_subdivision_fields
|
100
122
|
|
101
123
|
# Replace the subdivision values with the names of any predefined ones.
|
102
124
|
subdivision_fields.each_with_index.inject([{}, []]) do |(original_values, parents), (field, index)|
|
103
|
-
|
104
|
-
|
105
|
-
break
|
106
|
-
end
|
125
|
+
# This level is empty, so there can be no sublevels.
|
126
|
+
break if values[field].nil?
|
107
127
|
|
108
128
|
parents << ((index > 0) ? original_values[subdivision_fields[index - 1]] : address.country_code)
|
109
129
|
|
110
130
|
subdivision = Subdivision.get(values[field], parents)
|
111
|
-
if subdivision.nil?
|
112
|
-
break
|
113
|
-
end
|
131
|
+
break if subdivision.nil?
|
114
132
|
|
115
133
|
# Remember the original value so that it can be used for parents.
|
116
134
|
original_values[field] = values[field]
|
@@ -119,15 +137,11 @@ module Addressing
|
|
119
137
|
use_local_name = Locale.match_candidates(address.locale, subdivision.locale)
|
120
138
|
values[field] = use_local_name ? subdivision.local_code : subdivision.code
|
121
139
|
|
122
|
-
|
123
|
-
|
124
|
-
break
|
125
|
-
end
|
140
|
+
# The current subdivision has no children, stop.
|
141
|
+
break unless subdivision.children?
|
126
142
|
|
127
143
|
[original_values, parents]
|
128
144
|
end
|
129
|
-
|
130
|
-
values
|
131
145
|
end
|
132
146
|
|
133
147
|
private
|
@@ -142,7 +156,7 @@ module Addressing
|
|
142
156
|
end
|
143
157
|
end
|
144
158
|
|
145
|
-
if options.key?(:html) && !
|
159
|
+
if options.key?(:html) && ![true, false].include?(options[:html])
|
146
160
|
raise ArgumentError, "The option `html` must be a boolean."
|
147
161
|
end
|
148
162
|
|
data/lib/addressing/enum.rb
CHANGED
@@ -3,15 +3,14 @@
|
|
3
3
|
module Addressing
|
4
4
|
class Enum
|
5
5
|
class << self
|
6
|
-
@@values = {}
|
7
|
-
|
8
6
|
# Gets all available values.
|
9
7
|
def all
|
10
|
-
|
11
|
-
|
8
|
+
@values ||= {}
|
9
|
+
if !@values.key?(name)
|
10
|
+
@values[name] = constants.map { |constant| [constant, const_get(constant)] }.to_h
|
12
11
|
end
|
13
12
|
|
14
|
-
|
13
|
+
@values[name]
|
15
14
|
end
|
16
15
|
|
17
16
|
# Gets the key of the provided value.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Addressing
|
4
|
+
# Base error class for all Addressing errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised when an unknown country code is provided
|
8
|
+
class UnknownCountryError < Error
|
9
|
+
def initialize(country_code)
|
10
|
+
super("Unknown country code: #{country_code}")
|
11
|
+
@country_code = country_code
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :country_code
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when an unknown locale is provided
|
18
|
+
class UnknownLocaleError < Error
|
19
|
+
def initialize(locale)
|
20
|
+
super("Unknown locale: #{locale}")
|
21
|
+
@locale = locale
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :locale
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when an invalid argument is provided
|
28
|
+
class InvalidArgumentError < Error; end
|
29
|
+
end
|
@@ -14,20 +14,13 @@ module Addressing
|
|
14
14
|
AddressField.assert_all_exist(definition.keys)
|
15
15
|
FieldOverride.assert_all_exist(definition.values)
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
# Group fields by their override type
|
18
|
+
grouped = definition.group_by { |_field, override| override }
|
19
|
+
.transform_values { |pairs| pairs.map(&:first) }
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@hidden_fields << field
|
25
|
-
when FieldOverride::OPTIONAL
|
26
|
-
@optional_fields << field
|
27
|
-
when FieldOverride::REQUIRED
|
28
|
-
@required_fields << field
|
29
|
-
end
|
30
|
-
end
|
21
|
+
@hidden_fields = grouped[FieldOverride::HIDDEN] || []
|
22
|
+
@optional_fields = grouped[FieldOverride::OPTIONAL] || []
|
23
|
+
@required_fields = grouped[FieldOverride::REQUIRED] || []
|
31
24
|
end
|
32
25
|
end
|
33
26
|
end
|
data/lib/addressing/locale.rb
CHANGED
data/lib/addressing/model.rb
CHANGED
@@ -20,12 +20,6 @@ module Addressing
|
|
20
20
|
return unless address.country_code.present?
|
21
21
|
|
22
22
|
address_format = AddressFormat.get(address.country_code)
|
23
|
-
address_format.used_fields
|
24
|
-
|
25
|
-
return unless address.country_code.present?
|
26
|
-
|
27
|
-
address_format = AddressFormat.get(address.country_code)
|
28
|
-
address_format.used_fields
|
29
23
|
|
30
24
|
# Validate the presence of required fields.
|
31
25
|
AddressFormatHelper.required_fields(address_format, field_overrides).each do |required_field|
|
@@ -1,25 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Addressing
|
4
|
+
# Represents administrative subdivisions within countries.
|
5
|
+
#
|
6
|
+
# Subdivisions can be hierarchical with up to three levels:
|
7
|
+
# Administrative Area -> Locality -> Dependent Locality
|
8
|
+
#
|
9
|
+
# @example Get subdivisions for Brazil
|
10
|
+
# states = Addressing::Subdivision.all(['BR'])
|
11
|
+
# states.each do |code, state|
|
12
|
+
# puts "#{code}: #{state.name}"
|
13
|
+
# municipalities = state.children
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @example Get subdivisions for a Brazilian state
|
17
|
+
# municipalities = Addressing::Subdivision.all(['BR', 'CE'])
|
4
18
|
class Subdivision
|
5
19
|
class << self
|
6
|
-
# Subdivision
|
7
|
-
@@definitions = {}
|
8
|
-
|
9
|
-
# Parent subdivisions.
|
20
|
+
# Gets a Subdivision instance by ID and parent hierarchy.
|
10
21
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# memory usage.
|
15
|
-
@@parents = {}
|
16
|
-
|
22
|
+
# @param id [String] Subdivision ID
|
23
|
+
# @param parents [Array<String>] Parent hierarchy (e.g., ['BR'] or ['BR', 'CE'])
|
24
|
+
# @return [Subdivision, nil] Subdivision instance or nil if not found
|
17
25
|
def get(id, parents)
|
18
26
|
definitions = load_definitions(parents)
|
19
27
|
create_subdivision_from_definitions(id, definitions)
|
20
28
|
end
|
21
29
|
|
22
30
|
# Returns all subdivision instances for the provided parents.
|
31
|
+
#
|
32
|
+
# @param parents [Array<String>] Parent hierarchy (e.g., ['BR'] or ['BR', 'CE'])
|
33
|
+
# @return [Hash<String, Subdivision>] Hash of subdivision ID => Subdivision instance
|
23
34
|
def all(parents)
|
24
35
|
definitions = load_definitions(parents)
|
25
36
|
return {} if definitions.empty?
|
@@ -57,9 +68,10 @@ module Addressing
|
|
57
68
|
grandparents = parents.dup
|
58
69
|
parent_id = grandparents.pop
|
59
70
|
parent_group = build_group(grandparents.dup)
|
71
|
+
@definitions ||= {}
|
60
72
|
|
61
|
-
if
|
62
|
-
definition =
|
73
|
+
if @definitions.dig(parent_group, "subdivisions", parent_id)
|
74
|
+
definition = @definitions[parent_group]["subdivisions"][parent_id]
|
63
75
|
return !!definition["has_children"]
|
64
76
|
else
|
65
77
|
# The parent definition wasn't loaded previously, fallback to guessing based on depth.
|
@@ -73,12 +85,13 @@ module Addressing
|
|
73
85
|
|
74
86
|
# Loads the subdivision definitions for the provided parents.
|
75
87
|
def load_definitions(parents)
|
88
|
+
@definitions ||= {}
|
76
89
|
group = build_group(parents.dup)
|
77
|
-
if
|
78
|
-
return
|
90
|
+
if @definitions.key?(group)
|
91
|
+
return @definitions[group]
|
79
92
|
end
|
80
93
|
|
81
|
-
|
94
|
+
@definitions[group] = {}
|
82
95
|
|
83
96
|
# If there are predefined subdivisions at this level, try to load them.
|
84
97
|
if has_data(parents)
|
@@ -86,12 +99,12 @@ module Addressing
|
|
86
99
|
|
87
100
|
if File.exist?(filename)
|
88
101
|
raw_definition = File.read(filename)
|
89
|
-
|
90
|
-
|
102
|
+
@definitions[group] = JSON.parse(raw_definition)
|
103
|
+
@definitions[group] = process_definitions(@definitions[group])
|
91
104
|
end
|
92
105
|
end
|
93
106
|
|
94
|
-
|
107
|
+
@definitions[group]
|
95
108
|
end
|
96
109
|
|
97
110
|
# Processes the loaded definitions.
|
@@ -164,13 +177,14 @@ module Addressing
|
|
164
177
|
grandparents = parents.dup
|
165
178
|
parent_id = grandparents.pop
|
166
179
|
parent_group = build_group(grandparents.dup)
|
180
|
+
@parents ||= {}
|
167
181
|
|
168
|
-
if
|
169
|
-
|
170
|
-
|
182
|
+
if !@parents.dig(parent_group, parent_id)
|
183
|
+
@parents[parent_group] ||= {}
|
184
|
+
@parents[parent_group][parent_id] = get(parent_id, grandparents)
|
171
185
|
end
|
172
186
|
|
173
|
-
definition["parent"] =
|
187
|
+
definition["parent"] = @parents[parent_group][parent_id]
|
174
188
|
end
|
175
189
|
|
176
190
|
# Prepare children.
|
data/lib/addressing/version.rb
CHANGED
data/lib/addressing.rb
CHANGED
@@ -6,6 +6,7 @@ require "digest"
|
|
6
6
|
require "json"
|
7
7
|
|
8
8
|
# modules
|
9
|
+
require "addressing/exceptions"
|
9
10
|
require "addressing/enum"
|
10
11
|
require "addressing/address"
|
11
12
|
require "addressing/address_field"
|
@@ -23,14 +24,6 @@ require "addressing/postal_label_formatter"
|
|
23
24
|
require "addressing/subdivision"
|
24
25
|
require "addressing/version"
|
25
26
|
|
26
|
-
module Addressing
|
27
|
-
class Error < StandardError; end
|
28
|
-
|
29
|
-
class UnknownCountryError < Error; end
|
30
|
-
|
31
|
-
class UnknownLocaleError < Error; end
|
32
|
-
end
|
33
|
-
|
34
27
|
if defined?(ActiveSupport.on_load)
|
35
28
|
ActiveSupport.on_load(:active_record) do
|
36
29
|
require "addressing/model"
|
metadata
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: addressing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robin van der Vleuten
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
|
-
description:
|
14
12
|
email: robinvdvleuten@gmail.com
|
15
13
|
executables: []
|
16
14
|
extensions: []
|
@@ -712,6 +710,7 @@ files:
|
|
712
710
|
- lib/addressing/default_formatter.rb
|
713
711
|
- lib/addressing/dependent_locality_type.rb
|
714
712
|
- lib/addressing/enum.rb
|
713
|
+
- lib/addressing/exceptions.rb
|
715
714
|
- lib/addressing/field_override.rb
|
716
715
|
- lib/addressing/lazy_subdivisions.rb
|
717
716
|
- lib/addressing/locale.rb
|
@@ -727,7 +726,6 @@ licenses:
|
|
727
726
|
metadata:
|
728
727
|
bug_tracker_uri: https://github.com/robinvdvleuten/addressing/issues
|
729
728
|
changelog_uri: https://github.com/robinvdvleuten/addressing/blob/main/CHANGELOG.md
|
730
|
-
post_install_message:
|
731
729
|
rdoc_options: []
|
732
730
|
require_paths:
|
733
731
|
- lib
|
@@ -742,8 +740,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
742
740
|
- !ruby/object:Gem::Version
|
743
741
|
version: '0'
|
744
742
|
requirements: []
|
745
|
-
rubygems_version: 3.
|
746
|
-
signing_key:
|
743
|
+
rubygems_version: 3.6.8
|
747
744
|
specification_version: 4
|
748
745
|
summary: Addressing library powered by CLDR and Google's address data
|
749
746
|
test_files: []
|