usa 0.3.0 → 0.4.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 +20 -0
- data/README.md +74 -33
- data/app/models/{usa/city.rb → city.rb} +3 -6
- data/app/models/{usa/city_county.rb → city_county.rb} +6 -3
- data/app/models/{usa/county.rb → county.rb} +12 -11
- data/app/models/{usa/state.rb → state.rb} +11 -10
- data/app/models/usa/record.rb +4 -0
- data/app/models/{usa/zip.rb → zip.rb} +3 -6
- data/db/migrate/{20260914120000_create_usa_states.rb → 20260914120000_create_states.rb} +2 -2
- data/db/migrate/{20260914120001_create_usa_counties.rb → 20260914120001_create_counties.rb} +3 -3
- data/db/migrate/{20260914120002_create_usa_zips.rb → 20260914120002_create_zips.rb} +3 -3
- data/db/migrate/{20260914120003_create_usa_cities.rb → 20260914120003_create_cities.rb} +4 -3
- data/db/migrate/20260914120004_create_city_counties.rb +10 -0
- data/lib/usa/engine.rb +2 -2
- data/lib/usa/version.rb +1 -1
- data/lib/usa.rb +32 -4
- metadata +11 -11
- data/db/migrate/20260914120004_create_usa_city_counties.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03b2542ffafa029a4598cdcd0226b8c088fb86b581048ac9a0ad31143bf24e77
|
|
4
|
+
data.tar.gz: df5d33713273555f55bfef5f55b7c179095737de5aafb65a3ebaec7928ad78f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdcae36d574ba780eb0443dac939f3e3c01768a8bcb64e658eb0c8b4562cb6b2f6c53c2c16d8f59474fcc83b1eb9f0bfae192271a57027b2613c1ff97098f0bd
|
|
7
|
+
data.tar.gz: ca491f71bf66c03ca86ebe972122fd0b2afbf9fb8568cb64f0f3fd05089e334e91ba80ad65e62bae9d3ae2cc919189235a5c5ba40e64b0ab48ea42d267aeb88e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 0.4.0 - 2026-09-15
|
|
11
|
+
|
|
12
|
+
* [Breaking change] The models are `State`, `County`, `City`, `CityCounty` and `ZIP`, at the
|
|
13
|
+
top level rather than under `USA::`. A host writes `belongs_to :zip` and Active Record finds
|
|
14
|
+
the class, where before it refused anything but `class_name: 'USA::ZIP'` -- its `compute_type`
|
|
15
|
+
wants a class whose own name is the one it asked for, so no alias or configuration could stand
|
|
16
|
+
in. The `model_name` overrides that made the old classes answer as `ZIP` and `County` are gone
|
|
17
|
+
with the namespace they were papering over
|
|
18
|
+
* [Breaking change] The tables are `states`, `counties`, `cities`, `city_counties` and `zips`.
|
|
19
|
+
A host that wants them to itself sets `USA.table_name_prefix = 'usa_'` in an initializer and
|
|
20
|
+
gets the old names back -- one setting, read wherever a table is named, migrations included
|
|
21
|
+
* [Feature] `USA.verify_models` refuses a host whose own class stands where one of these models
|
|
22
|
+
should be, at boot and in one sentence. Zeitwerk gives an app's file precedence over an
|
|
23
|
+
engine's, silently, so a host holding `app/models/city.rb` would otherwise find every
|
|
24
|
+
association here pointing at a class this gem knows nothing about
|
|
25
|
+
* [Feature] `USA.seed` passes over a table the host never created, so an app that joins to three
|
|
26
|
+
of the five installs three and seeds three. `bin/rails db:usa:seed` goes on working there
|
|
27
|
+
* [Feature] `USA.table` names a table the way the host does, which is what the shipped migrations
|
|
28
|
+
now create and what a host's own migration can read
|
|
29
|
+
|
|
10
30
|
## 0.3.0 - 2026-09-14
|
|
11
31
|
|
|
12
32
|
* [Feature] Every model answers by the word a host means rather than by the table under it:
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ To install on your system, run
|
|
|
12
12
|
|
|
13
13
|
To use inside a bundled Ruby project, add this line to the `Gemfile`:
|
|
14
14
|
|
|
15
|
-
gem 'usa', '~> 0.
|
|
15
|
+
gem 'usa', '~> 0.4.0'
|
|
16
16
|
|
|
17
17
|
Below 1.0 the pin stops at the next minor rather than the next major, because that is where a
|
|
18
18
|
breaking change may still land. It becomes `~> 1.0` once the API is settled on purpose.
|
|
@@ -27,45 +27,85 @@ bin/rails db:migrate # the five tables, and every row in them
|
|
|
27
27
|
The last one takes about a minute: it writes 51 states, 3,144 counties, some 32,000 cities and
|
|
28
28
|
some 41,000 ZIPs.
|
|
29
29
|
|
|
30
|
+
Install only the tables you join to. Delete the migrations for the rest before you run them, and
|
|
31
|
+
`USA.seed` passes over what is not there — an app that never asks what city an address is in
|
|
32
|
+
keeps three tables rather than five.
|
|
33
|
+
|
|
30
34
|
**A database made from `db/schema.rb` has the tables and none of the rows.** A dump carries no
|
|
31
|
-
data, so `db:schema:load`, `db:test:prepare` and a fresh clone all leave the
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
data, so `db:schema:load`, `db:test:prepare` and a fresh clone all leave the tables empty. Run
|
|
36
|
+
`bin/rails db:usa:seed`, or call `USA.seed` from your own `db/seeds.rb`. The same command is how
|
|
37
|
+
a database catches up with a release that added rows: every write is an upsert keyed on the code
|
|
38
|
+
or the FIPS, so it inserts what is missing, updates what this gem owns, and leaves the id of
|
|
39
|
+
every row you already had exactly where it was.
|
|
36
40
|
|
|
37
41
|
## What you get
|
|
38
42
|
|
|
39
43
|
```ruby
|
|
40
|
-
zip =
|
|
41
|
-
zip.city
|
|
42
|
-
zip.time_zone
|
|
43
|
-
zip.county
|
|
44
|
-
zip.county.fips
|
|
45
|
-
zip.county.state.code
|
|
44
|
+
zip = ZIP.find_by code: '90210' # => #<ZIP>
|
|
45
|
+
zip.city # => 'Beverly Hills'
|
|
46
|
+
zip.time_zone # => 'Pacific Time (US & Canada)'
|
|
47
|
+
zip.county # => #<County>
|
|
48
|
+
zip.county.fips # => '06037'
|
|
49
|
+
zip.county.state.code # => 'CA'
|
|
46
50
|
|
|
47
51
|
county = zip.county
|
|
48
|
-
county.zips_count
|
|
49
|
-
county.cities
|
|
52
|
+
county.zips_count # => 508
|
|
53
|
+
county.cities # => [#<City>, ...]
|
|
50
54
|
|
|
51
55
|
state = county.state
|
|
52
|
-
state.counties_count
|
|
53
|
-
state.counties
|
|
54
|
-
state.cities
|
|
56
|
+
state.counties_count # => 58
|
|
57
|
+
state.counties # => [#<County>, ...]
|
|
58
|
+
state.cities # => [#<City>, ...]
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
A state has a `code`, a `fips` and a `name`. A county has a `fips`, a `name` and a state. A city
|
|
58
62
|
has a `fips` -- a Census place code, unique within its state rather than nationally -- a `name`,
|
|
59
63
|
a state, and the one or more counties it lies in, since a city may cross a county line. A ZIP has
|
|
60
64
|
a `code`, the `city` it is addressed as, a `time_zone` named as Rails names one, and one county.
|
|
65
|
+
|
|
66
|
+
That county and that city are the main one rather than the only one. A ZIP is a delivery route
|
|
67
|
+
rather than an area, and many of them cross a county line: this gem names the county the route
|
|
68
|
+
is chiefly in and the city it is chiefly addressed to, and holds nothing about the others. A
|
|
69
|
+
city is the exception, keeping every county it lies in, which is why `city.counties` answers
|
|
70
|
+
more than one.
|
|
71
|
+
|
|
61
72
|
Every one of the four also has a `google_place_id`, the ID Google gives the place, which is what
|
|
62
73
|
draws a table of them as a map. It is filled for every state, county and ZIP that Google keeps
|
|
63
74
|
as an area of its kind, and blank for the few it keeps only as a city -- the District of
|
|
64
75
|
Columbia, Broomfield County, Wrangell -- for the ZIPs it folds into a neighbor's, and for every
|
|
65
76
|
city until a release fills them.
|
|
66
77
|
|
|
67
|
-
Two counter caches are kept by the seed rather than by a callback: `
|
|
68
|
-
|
|
78
|
+
Two counter caches are kept by the seed rather than by a callback: `states.counties_count` and
|
|
79
|
+
`counties.zips_count`.
|
|
80
|
+
|
|
81
|
+
## The names are yours to write, not to choose
|
|
82
|
+
|
|
83
|
+
`State`, `County`, `City`, `CityCounty` and `ZIP` are this gem's classes, on `states`,
|
|
84
|
+
`counties`, `cities`, `city_counties` and `zips`. They are the words your routes, forms,
|
|
85
|
+
partials and locale keys already use, so `belongs_to :zip` finds the class, `zips_path` draws
|
|
86
|
+
the page and `zip[...]` names the field -- nothing has to be told which gem the model came from.
|
|
87
|
+
|
|
88
|
+
Which means this gem takes five names in your app, and a class of your own called `Zip`, `City`
|
|
89
|
+
or `State` cannot stand beside them. Rails gives your `app/models/city.rb` precedence over an
|
|
90
|
+
engine's, silently, so the gem checks at boot and refuses to start rather than let every
|
|
91
|
+
association here point at a class it knows nothing about:
|
|
92
|
+
|
|
93
|
+
The usa gem defines City, and a class of your own has taken the name. Rename yours: …
|
|
94
|
+
|
|
95
|
+
The gem also registers `USA`, `ZIP` and `FIPS` as acronyms, so `ZIP` is the spelling everywhere
|
|
96
|
+
-- a heading, a route helper, a migration's class name.
|
|
97
|
+
|
|
98
|
+
If you want these tables to themselves, name them:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# config/initializers/usa.rb
|
|
102
|
+
USA.table_name_prefix = 'usa_'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
One setting, read wherever a table is named: the models, the counter queries, and the migrations
|
|
106
|
+
the generator hands you. Set it before anything queries, which an initializer is. The class
|
|
107
|
+
names are not configurable -- Active Record resolves `belongs_to :zip` by asking for a class
|
|
108
|
+
called `ZIP`, and nothing but a class of that name will do.
|
|
69
109
|
|
|
70
110
|
## Making them yours
|
|
71
111
|
|
|
@@ -80,31 +120,32 @@ ActiveSupport.on_load(:usa_zip) do
|
|
|
80
120
|
end
|
|
81
121
|
```
|
|
82
122
|
|
|
83
|
-
`:usa_state`, `:usa_county`, `:usa_city` and `:usa_zip` are the
|
|
84
|
-
an app says how all of them connect -- a reading role, say, which these
|
|
85
|
-
nothing about:
|
|
123
|
+
`:usa_state`, `:usa_county`, `:usa_city`, `:usa_city_county` and `:usa_zip` are the five, and
|
|
124
|
+
`:usa_record` is where an app says how all of them connect -- a reading role, say, which these
|
|
125
|
+
models otherwise know nothing about:
|
|
86
126
|
|
|
87
127
|
```ruby
|
|
88
128
|
ActiveSupport.on_load(:usa_record) { connects_to database: { writing: :primary, reading: :reader } }
|
|
89
129
|
```
|
|
90
130
|
|
|
91
|
-
Each model answers by the word you mean rather than by the table under it, so a page listing
|
|
92
|
-
ZIPs writes `zips_path`, a form posts `zip[...]`, a partial lives at `zips/_row` and a locale
|
|
93
|
-
key reads `zip` -- while the table stays `usa_zips`. A gem that resolves a model from a route
|
|
94
|
-
finds it without being told.
|
|
95
|
-
|
|
96
131
|
Columns of your own go on these tables in a migration of your own. They survive every seed: this
|
|
97
132
|
gem writes only the columns it ships.
|
|
98
133
|
|
|
99
134
|
## Adopting it in an app that already has these tables
|
|
100
135
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
Your tables are already called `states`, `counties` and `zips`, so there is nothing to rename:
|
|
137
|
+
add the columns this gem's own carry that yours lack -- `google_place_id`, the counter caches,
|
|
138
|
+
a ZIP's city and time zone -- and seed. Every id stays where it is, because the seed upserts on
|
|
139
|
+
the code or the FIPS rather than on the id, and every foreign key of yours goes on pointing at
|
|
140
|
+
the row it pointed at.
|
|
141
|
+
|
|
142
|
+
Delete the models you had for them, and say through the load hooks what they said. Where your
|
|
143
|
+
rows were loaded with explicit ids, run `setval` on the sequence before the first seed, or the
|
|
144
|
+
first insert collides.
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
Two things to know. A class of yours called `Zip` stops being found the day you install this,
|
|
147
|
+
which the boot check will tell you. And `State` and `County` cascade to `cities` with
|
|
148
|
+
`dependent: :destroy`, so an app that skipped those tables must not destroy one.
|
|
108
149
|
|
|
109
150
|
## Development
|
|
110
151
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# A city, town or census designated place: what a street address is addressed to.
|
|
2
|
-
class
|
|
2
|
+
class City < USA::Record
|
|
3
3
|
include USA::Seeded
|
|
4
4
|
|
|
5
5
|
belongs_to :state
|
|
@@ -14,16 +14,13 @@ class USA::City < USA::Record
|
|
|
14
14
|
# @return [String] the default representation (used in views).
|
|
15
15
|
def to_s = "#{name} (#{state.code})"
|
|
16
16
|
|
|
17
|
-
# The name Rails reads off this model: its route, its param key, its partial, its key.
|
|
18
|
-
def self.model_name = ActiveModel::Name.new(self, nil, 'City')
|
|
19
|
-
|
|
20
17
|
class << self
|
|
21
18
|
private
|
|
22
19
|
|
|
23
20
|
def natural_key = %i[state_id fips]
|
|
24
21
|
|
|
25
22
|
def seeds
|
|
26
|
-
states =
|
|
23
|
+
states = State.pluck(:code, :id).to_h
|
|
27
24
|
csv('cities').lazy.map do |row|
|
|
28
25
|
{ fips: row['fips'], name: row['name'], state_id: states.fetch(row['state']),
|
|
29
26
|
google_place_id: row['google_place_id'], }
|
|
@@ -32,4 +29,4 @@ class USA::City < USA::Record
|
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
31
|
|
|
35
|
-
ActiveSupport.run_load_hooks :usa_city,
|
|
32
|
+
ActiveSupport.run_load_hooks :usa_city, City
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Where a city lies: one row per county it reaches into, since a city may span several.
|
|
2
|
-
class
|
|
2
|
+
class CityCounty < USA::Record
|
|
3
3
|
include USA::Seeded
|
|
4
4
|
|
|
5
5
|
belongs_to :city
|
|
@@ -11,9 +11,10 @@ class USA::CityCounty < USA::Record
|
|
|
11
11
|
def natural_key = %i[city_id county_id]
|
|
12
12
|
|
|
13
13
|
def seeds
|
|
14
|
-
cities =
|
|
14
|
+
cities = City.joins(:state).
|
|
15
|
+
pluck("#{State.table_name}.code", :fips, "#{City.table_name}.id").
|
|
15
16
|
to_h { |code, fips, id| [ [ code, fips ], id ] }
|
|
16
|
-
counties =
|
|
17
|
+
counties = County.pluck(:fips, :id).to_h
|
|
17
18
|
csv('cities').lazy.flat_map do |row|
|
|
18
19
|
city_id = cities.fetch [ row['state'], row['fips'] ]
|
|
19
20
|
row['counties'].split.map { |fips| { city_id:, county_id: counties.fetch(fips) } }
|
|
@@ -21,3 +22,5 @@ class USA::CityCounty < USA::Record
|
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
end
|
|
25
|
+
|
|
26
|
+
ActiveSupport.run_load_hooks :usa_city_county, CityCounty
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# A county, parish, borough or census area: the division every ZIP is counted under.
|
|
2
|
-
class
|
|
2
|
+
class County < USA::Record
|
|
3
3
|
include USA::Seeded
|
|
4
4
|
|
|
5
5
|
belongs_to :state, counter_cache: true
|
|
@@ -11,29 +11,30 @@ class USA::County < USA::Record
|
|
|
11
11
|
validates :fips, presence: true, length: { is: 5 }, uniqueness: true
|
|
12
12
|
validates :name, presence: true
|
|
13
13
|
|
|
14
|
-
# How many ZIPs a county holds, asked of the rows: an upsert runs no callback to keep it.
|
|
15
|
-
ZIPS = 'SELECT COUNT(*) FROM usa_zips WHERE usa_zips.county_id = usa_counties.id'
|
|
16
|
-
|
|
17
14
|
# Counts the ZIPs of every county the count is wrong for, and touches none of the others.
|
|
18
15
|
# @return [void]
|
|
19
16
|
def self.recount_zips
|
|
20
|
-
where("zips_count <> (#{
|
|
21
|
-
update_all [ "zips_count = (#{
|
|
17
|
+
where("zips_count <> (#{counted_zips})").
|
|
18
|
+
update_all [ "zips_count = (#{counted_zips}), updated_at = ?", Time.current ]
|
|
22
19
|
end
|
|
23
20
|
|
|
24
21
|
# @return [String] the default representation (used in views).
|
|
25
22
|
def to_s = "#{name} (#{state.code})"
|
|
26
23
|
|
|
27
|
-
# The name Rails reads off this model: its route, its param key, its partial, its key.
|
|
28
|
-
def self.model_name = ActiveModel::Name.new(self, nil, 'County')
|
|
29
|
-
|
|
30
24
|
class << self
|
|
31
25
|
private
|
|
32
26
|
|
|
27
|
+
# How many ZIPs a county holds, asked of the rows: an upsert runs no callback to keep it.
|
|
28
|
+
def counted_zips
|
|
29
|
+
theirs = ZIP.table_name
|
|
30
|
+
|
|
31
|
+
"SELECT COUNT(*) FROM #{theirs} WHERE #{theirs}.county_id = #{table_name}.id"
|
|
32
|
+
end
|
|
33
|
+
|
|
33
34
|
def natural_key = :fips
|
|
34
35
|
|
|
35
36
|
def seeds
|
|
36
|
-
states =
|
|
37
|
+
states = State.pluck(:code, :id).to_h
|
|
37
38
|
csv('counties').lazy.map do |row|
|
|
38
39
|
{ fips: row['fips'], name: row['name'], state_id: states.fetch(row['state']),
|
|
39
40
|
google_place_id: row['google_place_id'], }
|
|
@@ -42,4 +43,4 @@ class USA::County < USA::Record
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
ActiveSupport.run_load_hooks :usa_county,
|
|
46
|
+
ActiveSupport.run_load_hooks :usa_county, County
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# A state of the United States, or the District of Columbia.
|
|
2
|
-
class
|
|
2
|
+
class State < USA::Record
|
|
3
3
|
include USA::Seeded
|
|
4
4
|
|
|
5
5
|
has_many :counties, dependent: :destroy
|
|
@@ -9,25 +9,26 @@ class USA::State < USA::Record
|
|
|
9
9
|
validates :fips, presence: true, length: { is: 2 }, uniqueness: true
|
|
10
10
|
validates :name, presence: true, uniqueness: true
|
|
11
11
|
|
|
12
|
-
# How many counties a state holds, asked of the rows: an upsert runs no callback to keep it.
|
|
13
|
-
COUNTIES = 'SELECT COUNT(*) FROM usa_counties WHERE usa_counties.state_id = usa_states.id'
|
|
14
|
-
|
|
15
12
|
# Counts the counties of every state the count is wrong for, and touches none of the others.
|
|
16
13
|
# @return [void]
|
|
17
14
|
def self.recount_counties
|
|
18
|
-
where("counties_count <> (#{
|
|
19
|
-
update_all [ "counties_count = (#{
|
|
15
|
+
where("counties_count <> (#{counted_counties})").
|
|
16
|
+
update_all [ "counties_count = (#{counted_counties}), updated_at = ?", Time.current ]
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
# @return [String] the default representation (used in views).
|
|
23
20
|
def to_s = name
|
|
24
21
|
|
|
25
|
-
# The name Rails reads off this model: its route, its param key, its partial, its key.
|
|
26
|
-
def self.model_name = ActiveModel::Name.new(self, nil, 'State')
|
|
27
|
-
|
|
28
22
|
class << self
|
|
29
23
|
private
|
|
30
24
|
|
|
25
|
+
# How many counties a state holds, asked of the rows: an upsert runs no callback to keep it.
|
|
26
|
+
def counted_counties
|
|
27
|
+
theirs = County.table_name
|
|
28
|
+
|
|
29
|
+
"SELECT COUNT(*) FROM #{theirs} WHERE #{theirs}.state_id = #{table_name}.id"
|
|
30
|
+
end
|
|
31
|
+
|
|
31
32
|
def natural_key = :code
|
|
32
33
|
|
|
33
34
|
def seeds
|
|
@@ -39,4 +40,4 @@ class USA::State < USA::Record
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
ActiveSupport.run_load_hooks :usa_state,
|
|
43
|
+
ActiveSupport.run_load_hooks :usa_state, State
|
data/app/models/usa/record.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# What every record this gem holds descends from, so a host says in one line how they connect.
|
|
2
2
|
class USA::Record < ActiveRecord::Base
|
|
3
3
|
self.abstract_class = true
|
|
4
|
+
|
|
5
|
+
# Read rather than stored, so an initializer setting it is in time whenever a model loads.
|
|
6
|
+
# @return [String] what this gem's tables are named with.
|
|
7
|
+
def self.table_name_prefix = USA.table_name_prefix
|
|
4
8
|
end
|
|
5
9
|
|
|
6
10
|
ActiveSupport.run_load_hooks :usa_record, USA::Record
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# A ZIP code, and the city, time zone and county the Postal Service delivers it in.
|
|
2
|
-
class
|
|
2
|
+
class ZIP < USA::Record
|
|
3
3
|
include USA::Seeded
|
|
4
4
|
|
|
5
5
|
belongs_to :county, counter_cache: true
|
|
@@ -11,16 +11,13 @@ class USA::ZIP < USA::Record
|
|
|
11
11
|
# @return [String] the default representation (used in views).
|
|
12
12
|
def to_s = code
|
|
13
13
|
|
|
14
|
-
# The name Rails reads off this model: its route, its param key, its partial, its key.
|
|
15
|
-
def self.model_name = ActiveModel::Name.new(self, nil, 'ZIP')
|
|
16
|
-
|
|
17
14
|
class << self
|
|
18
15
|
private
|
|
19
16
|
|
|
20
17
|
def natural_key = :code
|
|
21
18
|
|
|
22
19
|
def seeds
|
|
23
|
-
counties =
|
|
20
|
+
counties = County.pluck(:fips, :id).to_h
|
|
24
21
|
csv('zips').lazy.map do |row|
|
|
25
22
|
{ code: row['code'], city: row['city'], time_zone: row['time_zone'],
|
|
26
23
|
county_id: counties.fetch(row['county']), google_place_id: row['google_place_id'], }
|
|
@@ -29,4 +26,4 @@ class USA::ZIP < USA::Record
|
|
|
29
26
|
end
|
|
30
27
|
end
|
|
31
28
|
|
|
32
|
-
ActiveSupport.run_load_hooks :usa_zip,
|
|
29
|
+
ActiveSupport.run_load_hooks :usa_zip, ZIP
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreateStates < ActiveRecord::Migration[8.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table USA.table(:states) do |t|
|
|
4
4
|
t.string :code, limit: 2, null: false, index: { unique: true }
|
|
5
5
|
t.string :fips, limit: 2, null: false, index: { unique: true }
|
|
6
6
|
t.string :name, null: false, index: { unique: true }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreateCounties < ActiveRecord::Migration[8.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table USA.table(:counties) do |t|
|
|
4
4
|
t.string :fips, limit: 5, null: false, index: { unique: true }
|
|
5
5
|
t.string :name, null: false, index: true
|
|
6
|
-
t.references :state, null: false, foreign_key: { to_table: :
|
|
6
|
+
t.references :state, null: false, foreign_key: { to_table: USA.table(:states) }
|
|
7
7
|
t.integer :zips_count, default: 0, null: false
|
|
8
8
|
t.string :google_place_id
|
|
9
9
|
t.timestamps
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreateZips < ActiveRecord::Migration[8.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table USA.table(:zips) do |t|
|
|
4
4
|
t.string :code, limit: 5, null: false, index: { unique: true }
|
|
5
5
|
t.string :city, null: false, index: true
|
|
6
6
|
t.string :time_zone, null: false
|
|
7
|
-
t.references :county, null: false, foreign_key: { to_table: :
|
|
7
|
+
t.references :county, null: false, foreign_key: { to_table: USA.table(:counties) }
|
|
8
8
|
t.string :google_place_id
|
|
9
9
|
t.timestamps
|
|
10
10
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreateCities < ActiveRecord::Migration[8.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table USA.table(:cities) do |t|
|
|
4
4
|
t.string :fips, limit: 5, null: false
|
|
5
5
|
t.string :name, null: false, index: true
|
|
6
|
-
t.references :state, null: false, index: false,
|
|
6
|
+
t.references :state, null: false, index: false,
|
|
7
|
+
foreign_key: { to_table: USA.table(:states) }
|
|
7
8
|
t.string :google_place_id
|
|
8
9
|
t.timestamps
|
|
9
10
|
t.index %i[state_id fips], unique: true
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class CreateCityCounties < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table USA.table(:city_counties), id: false do |t|
|
|
4
|
+
t.references :city, null: false, index: false,
|
|
5
|
+
foreign_key: { to_table: USA.table(:cities) }
|
|
6
|
+
t.references :county, null: false, foreign_key: { to_table: USA.table(:counties) }
|
|
7
|
+
t.index %i[city_id county_id], unique: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/usa/engine.rb
CHANGED
|
@@ -2,8 +2,8 @@ require 'action_dispatch'
|
|
|
2
2
|
require 'rails/engine'
|
|
3
3
|
|
|
4
4
|
module USA
|
|
5
|
-
# Teaches Rails where this gem's models live, and
|
|
5
|
+
# Teaches Rails where this gem's models live, and refuses a host that has taken their names.
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
|
-
|
|
7
|
+
config.to_prepare { USA.verify_models ::State, ::County, ::City, ::CityCounty, ::ZIP }
|
|
8
8
|
end
|
|
9
9
|
end
|
data/lib/usa/version.rb
CHANGED
data/lib/usa.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'active_support/inflector'
|
|
2
2
|
require 'csv'
|
|
3
3
|
|
|
4
|
-
# Before the engine, so Zeitwerk reads
|
|
4
|
+
# Before the engine, so Zeitwerk reads zip.rb as ZIP and a heading says ZIP, not Zip.
|
|
5
5
|
ActiveSupport::Inflector.inflections do |inflect|
|
|
6
6
|
inflect.acronym 'USA'
|
|
7
7
|
inflect.acronym 'ZIP'
|
|
@@ -13,11 +13,39 @@ require 'usa/engine'
|
|
|
13
13
|
|
|
14
14
|
# The geography of the United States: the states, counties, cities and ZIPs an address names.
|
|
15
15
|
module USA
|
|
16
|
+
# What every table this gem ships is named with. Empty, so a host joins to `states` and
|
|
17
|
+
# `zips`; set it to `usa_` from an initializer to keep this gem's tables to themselves.
|
|
18
|
+
mattr_accessor :table_name_prefix, default: ''
|
|
19
|
+
|
|
20
|
+
# Raised for every failure this gem reports, so a host can rescue one type.
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
# The name one of this gem's tables goes by in the host, which is what its migrations create.
|
|
24
|
+
# @return [Symbol] prefixed table name.
|
|
25
|
+
def self.table(name) = :"#{table_name_prefix}#{name}"
|
|
26
|
+
|
|
16
27
|
# Writes every row this release holds, keeping the id of every row the database already has.
|
|
28
|
+
# A table the host never created is passed over, so an app that joins to three of the five
|
|
29
|
+
# installs three and seeds them.
|
|
17
30
|
# @return [void]
|
|
18
31
|
def self.seed
|
|
19
|
-
[
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
[ State, County, City, CityCounty, ZIP ].select(&:table_exists?).each(&:seed)
|
|
33
|
+
State.recount_counties
|
|
34
|
+
County.recount_zips
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Refuses a host's own class standing where one of this gem's models should be. Zeitwerk
|
|
38
|
+
# gives an app's file precedence over an engine's, silently, so without this a host holding
|
|
39
|
+
# `app/models/city.rb` would find every association of this gem's pointing at a class of its
|
|
40
|
+
# own.
|
|
41
|
+
# @param [Array<Class>] models the classes the names this gem takes answer to.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def self.verify_models(*models)
|
|
44
|
+
theirs = models.reject { |model| model < Record }
|
|
45
|
+
return if theirs.empty?
|
|
46
|
+
|
|
47
|
+
raise Error, "The usa gem defines #{theirs.to_sentence}, and a class of your own has " \
|
|
48
|
+
'taken the name. Rename yours: Rails gives an app’s file precedence over an engine’s, ' \
|
|
49
|
+
'silently, so the models this gem ships would point at a class it knows nothing about.'
|
|
22
50
|
end
|
|
23
51
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: usa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -47,18 +47,18 @@ files:
|
|
|
47
47
|
- CHANGELOG.md
|
|
48
48
|
- LICENSE.txt
|
|
49
49
|
- README.md
|
|
50
|
+
- app/models/city.rb
|
|
51
|
+
- app/models/city_county.rb
|
|
50
52
|
- app/models/concerns/usa/seeded.rb
|
|
51
|
-
- app/models/
|
|
52
|
-
- app/models/
|
|
53
|
-
- app/models/usa/county.rb
|
|
53
|
+
- app/models/county.rb
|
|
54
|
+
- app/models/state.rb
|
|
54
55
|
- app/models/usa/record.rb
|
|
55
|
-
- app/models/
|
|
56
|
-
-
|
|
57
|
-
- db/migrate/
|
|
58
|
-
- db/migrate/
|
|
59
|
-
- db/migrate/
|
|
60
|
-
- db/migrate/
|
|
61
|
-
- db/migrate/20260914120004_create_usa_city_counties.rb
|
|
56
|
+
- app/models/zip.rb
|
|
57
|
+
- db/migrate/20260914120000_create_states.rb
|
|
58
|
+
- db/migrate/20260914120001_create_counties.rb
|
|
59
|
+
- db/migrate/20260914120002_create_zips.rb
|
|
60
|
+
- db/migrate/20260914120003_create_cities.rb
|
|
61
|
+
- db/migrate/20260914120004_create_city_counties.rb
|
|
62
62
|
- db/migrate/20260914120005_seed_usa.rb
|
|
63
63
|
- db/seeds/cities.csv
|
|
64
64
|
- db/seeds/counties.csv
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
class CreateUSACityCounties < ActiveRecord::Migration[8.1]
|
|
2
|
-
def change
|
|
3
|
-
create_table :usa_city_counties, id: false do |t|
|
|
4
|
-
t.references :city, null: false, index: false, foreign_key: { to_table: :usa_cities }
|
|
5
|
-
t.references :county, null: false, foreign_key: { to_table: :usa_counties }
|
|
6
|
-
t.index %i[city_id county_id], unique: true
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
end
|