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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: baafe271decb488334f07d38d4312f97c37a031cbfe8d142fc91d0c6b9de0f9a
4
- data.tar.gz: 17800009d94fccacc4f14845549909084552018624996963fed80d72630a79d6
3
+ metadata.gz: 03b2542ffafa029a4598cdcd0226b8c088fb86b581048ac9a0ad31143bf24e77
4
+ data.tar.gz: df5d33713273555f55bfef5f55b7c179095737de5aafb65a3ebaec7928ad78f1
5
5
  SHA512:
6
- metadata.gz: 91179ce7f344c77569bcee54f8b15a4c0afd2781f7658a8d225ea353a4b44d43ddca5c9d1efbbdcfacf1eb8ec062c455730364bc99893571b9962961ea7a4284
7
- data.tar.gz: bdac914175b069d651ea61be39d48896cee8401506a415f7a399d7607226f9a7175f30fd7b8f3c09d243253dcfae958be3b41941b52cc9ffafc87c9e6327a17e
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.3.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 five tables empty.
32
- Run `bin/rails db:usa:seed`, or call `USA.seed` from your own `db/seeds.rb`. The same command is
33
- how a database catches up with a release that added rows: every write is an upsert keyed on the
34
- code or the FIPS, so it inserts what is missing, updates what this gem owns, and leaves the id
35
- of every row you already had exactly where it was.
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 = USA::ZIP.find_by code: '90210' # => #<USA::ZIP>
41
- zip.city # => 'Beverly Hills'
42
- zip.time_zone # => 'Pacific Time (US & Canada)'
43
- zip.county # => #<USA::County>
44
- zip.county.fips # => '06037'
45
- zip.county.state.code # => 'CA'
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 # => 508
49
- county.cities # => [#<USA::City>, ...]
52
+ county.zips_count # => 508
53
+ county.cities # => [#<City>, ...]
50
54
 
51
55
  state = county.state
52
- state.counties_count # => 58
53
- state.counties # => [#<USA::County>, ...]
54
- state.cities # => [#<USA::City>, ...]
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: `usa_states.counties_count`
68
- and `usa_counties.zips_count`.
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 four, and `:usa_record` is where
84
- an app says how all of them connect -- a reading role, say, which these models otherwise know
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
- The gem registers `USA`, `ZIP` and `FIPS` as acronyms, so a class of yours called `Zip` stops
102
- being found the day you install it -- rename it, or reach `USA::ZIP` instead. Point your foreign
103
- keys at `usa_zips.id` rather than at a `zips` of your own, and where your rows were loaded with
104
- explicit ids, run `setval` on the sequence before the first seed, or the first insert collides.
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
- The gem does not support an app that sets `ActiveRecord::Base.table_name_prefix`: its migrations
107
- name `usa_states` while the models would look for the prefix and yours together.
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 USA::City < USA::Record
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 = USA::State.pluck(:code, :id).to_h
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, 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 USA::CityCounty < USA::Record
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 = USA::City.joins(:state).pluck('usa_states.code', :fips, 'usa_cities.id').
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 = USA::County.pluck(:fips, :id).to_h
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 USA::County < USA::Record
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 <> (#{ZIPS})").
21
- update_all [ "zips_count = (#{ZIPS}), updated_at = ?", Time.current ]
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 = USA::State.pluck(:code, :id).to_h
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, 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 USA::State < USA::Record
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 <> (#{COUNTIES})").
19
- update_all [ "counties_count = (#{COUNTIES}), updated_at = ?", Time.current ]
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, USA::State
43
+ ActiveSupport.run_load_hooks :usa_state, State
@@ -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 USA::ZIP < USA::Record
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 = USA::County.pluck(:fips, :id).to_h
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, USA::ZIP
29
+ ActiveSupport.run_load_hooks :usa_zip, ZIP
@@ -1,6 +1,6 @@
1
- class CreateUSAStates < ActiveRecord::Migration[8.1]
1
+ class CreateStates < ActiveRecord::Migration[8.1]
2
2
  def change
3
- create_table :usa_states do |t|
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 CreateUSACounties < ActiveRecord::Migration[8.1]
1
+ class CreateCounties < ActiveRecord::Migration[8.1]
2
2
  def change
3
- create_table :usa_counties do |t|
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: :usa_states }
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 CreateUSAZips < ActiveRecord::Migration[8.1]
1
+ class CreateZips < ActiveRecord::Migration[8.1]
2
2
  def change
3
- create_table :usa_zips do |t|
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: :usa_counties }
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 CreateUSACities < ActiveRecord::Migration[8.1]
1
+ class CreateCities < ActiveRecord::Migration[8.1]
2
2
  def change
3
- create_table :usa_cities do |t|
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, foreign_key: { to_table: :usa_states }
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 prefixes their tables with `usa_`.
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
- isolate_namespace USA
7
+ config.to_prepare { USA.verify_models ::State, ::County, ::City, ::CityCounty, ::ZIP }
8
8
  end
9
9
  end
data/lib/usa/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module USA
2
2
  # The version of this gem, as RubyGems knows it.
3
- VERSION = '0.3.0'
3
+ VERSION = '0.4.0'
4
4
  end
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 usa/zip.rb as USA::ZIP and a heading says ZIP, not Zip.
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
- [ USA::State, USA::County, USA::City, USA::CityCounty, USA::ZIP ].each(&:seed)
20
- USA::State.recount_counties
21
- USA::County.recount_zips
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.3.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/usa/city.rb
52
- - app/models/usa/city_county.rb
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/usa/state.rb
56
- - app/models/usa/zip.rb
57
- - db/migrate/20260914120000_create_usa_states.rb
58
- - db/migrate/20260914120001_create_usa_counties.rb
59
- - db/migrate/20260914120002_create_usa_zips.rb
60
- - db/migrate/20260914120003_create_usa_cities.rb
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