free_zipcode_data 1.0.6 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +25 -16
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG +11 -0
  6. data/CLAUDE.md +89 -0
  7. data/Gemfile +10 -0
  8. data/Gemfile.lock +50 -36
  9. data/README.md +3 -5
  10. data/Rakefile +1 -1
  11. data/free_zipcode_data.gemspec +8 -14
  12. data/lib/etl/common.rb +1 -0
  13. data/lib/etl/csv_source.rb +4 -4
  14. data/lib/free_zipcode_data/country_table.rb +10 -2
  15. data/lib/free_zipcode_data/county_table.rb +14 -6
  16. data/lib/free_zipcode_data/data_source.rb +2 -2
  17. data/lib/free_zipcode_data/db_table.rb +54 -7
  18. data/lib/free_zipcode_data/logger.rb +8 -12
  19. data/lib/free_zipcode_data/runner.rb +2 -2
  20. data/lib/free_zipcode_data/state_table.rb +37 -5
  21. data/lib/free_zipcode_data/version.rb +1 -1
  22. data/lib/free_zipcode_data/zipcode_table.rb +15 -5
  23. data/lib/free_zipcode_data.rb +3 -3
  24. data/lib/tasks/version.rake +27 -24
  25. data/spec/etl/csv_source_spec.rb +57 -0
  26. data/spec/etl/free_zipcode_data_job_spec.rb +135 -0
  27. data/spec/fixtures/.free_zipcode_data.yml +1 -0
  28. data/spec/fixtures/US.txt +5 -0
  29. data/spec/fixtures/US.zip +0 -0
  30. data/spec/fixtures/test_data.csv +7 -0
  31. data/spec/fixtures/test_data.txt +5 -0
  32. data/spec/free_zipcode_data/country_table_spec.rb +52 -0
  33. data/spec/free_zipcode_data/county_table_spec.rb +84 -0
  34. data/spec/free_zipcode_data/data_source_spec.rb +131 -0
  35. data/spec/free_zipcode_data/db_table_spec.rb +164 -0
  36. data/spec/free_zipcode_data/logger_spec.rb +78 -0
  37. data/spec/free_zipcode_data/options_spec.rb +37 -0
  38. data/spec/free_zipcode_data/runner_spec.rb +91 -0
  39. data/spec/free_zipcode_data/sqlite_ram_spec.rb +64 -0
  40. data/spec/free_zipcode_data/state_table_spec.rb +112 -0
  41. data/spec/free_zipcode_data/zipcode_table_spec.rb +102 -0
  42. data/spec/free_zipcode_data_spec.rb +38 -0
  43. data/spec/spec_helper.rb +23 -2
  44. data/spec/support/database_helpers.rb +48 -0
  45. metadata +38 -91
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'free_zipcode_data/zipcode_table'
4
+
5
+ RSpec.describe FreeZipcodeData::ZipcodeTable do
6
+ let(:db) { create_test_database(line_count: 5) }
7
+ let(:table) { described_class.new(database: db, tablename: 'zipcodes') }
8
+
9
+ before do
10
+ seed_countries(db)
11
+ seed_states(db)
12
+ table.build
13
+ end
14
+
15
+ describe '#build' do
16
+ it 'creates the zipcodes table' do
17
+ tables = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='zipcodes'")
18
+ expect(tables.length).to eq(1)
19
+ end
20
+
21
+ it 'creates the unique_zipcode index' do
22
+ indexes = db.execute("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='zipcodes'")
23
+ index_names = indexes.map(&:first)
24
+ expect(index_names).to include('unique_zipcode')
25
+ end
26
+
27
+ it 'creates columns for code, state_id, city, area_code, lat, lon, accuracy' do
28
+ columns = db.execute("PRAGMA table_info('zipcodes')").map { |c| c[1] }
29
+ expect(columns).to include('code', 'state_id', 'city', 'area_code', 'lat', 'lon', 'accuracy')
30
+ end
31
+ end
32
+
33
+ describe '#write' do
34
+ let(:row) do
35
+ {
36
+ country: 'US',
37
+ postal_code: '60601',
38
+ short_state: 'IL',
39
+ state: 'Illinois',
40
+ city: 'Chicago',
41
+ latitude: '41.8819',
42
+ longitude: '-87.6278',
43
+ accuracy: '4'
44
+ }
45
+ end
46
+
47
+ it 'inserts a zipcode row' do
48
+ table.write(row)
49
+ rows = db.execute('SELECT code, city FROM zipcodes')
50
+ expect(rows.length).to eq(1)
51
+ expect(rows[0]).to eq(%w[60601 Chicago])
52
+ end
53
+
54
+ it 'stores latitude and longitude' do
55
+ table.write(row)
56
+ rows = db.execute('SELECT lat, lon FROM zipcodes')
57
+ expect(rows[0][0].to_s).to start_with('41.88')
58
+ expect(rows[0][1].to_s).to start_with('-87.62')
59
+ end
60
+
61
+ it 'links the zipcode to its state' do
62
+ table.write(row)
63
+ state_id = db.execute("SELECT id FROM states WHERE abbr = 'IL'")[0][0]
64
+ zipcode_state_id = db.execute('SELECT state_id FROM zipcodes')[0][0]
65
+ expect(zipcode_state_id.to_i).to eq(state_id)
66
+ end
67
+
68
+ it 'returns nil and skips when postal_code is nil' do
69
+ result = table.write(row.merge(postal_code: nil))
70
+ expect(result).to be_nil
71
+ rows = db.execute('SELECT COUNT(*) FROM zipcodes')
72
+ expect(rows[0][0]).to eq(0)
73
+ end
74
+
75
+ it 'returns nil and skips when state cannot be found' do
76
+ result = table.write(row.merge(short_state: 'ZZ', state: 'Nonexistent'))
77
+ expect(result).to be_nil
78
+ rows = db.execute('SELECT COUNT(*) FROM zipcodes')
79
+ expect(rows[0][0]).to eq(0)
80
+ end
81
+
82
+ it 'silently ignores duplicate zipcode entries' do
83
+ table.write(row)
84
+ expect { table.write(row) }.not_to raise_error
85
+ rows = db.execute('SELECT COUNT(*) FROM zipcodes')
86
+ expect(rows[0][0]).to eq(1)
87
+ end
88
+
89
+ it 'handles city names with single quotes' do
90
+ table.write(row.merge(city: "Coeur d'Alene", postal_code: '83814'))
91
+ rows = db.execute('SELECT city FROM zipcodes')
92
+ expect(rows[0][0]).to eq("Coeur d'Alene")
93
+ end
94
+
95
+ it 'inserts multiple different zipcodes' do
96
+ table.write(row)
97
+ table.write(row.merge(postal_code: '10001', city: 'New York', short_state: 'NY', state: 'New York'))
98
+ rows = db.execute('SELECT COUNT(*) FROM zipcodes')
99
+ expect(rows[0][0]).to eq(2)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FreeZipcodeData do
4
+ describe '.root' do
5
+ it 'returns a Pathname to the project root' do
6
+ expect(described_class.root).to be_a(Pathname)
7
+ expect(described_class.root.join('lib', 'free_zipcode_data.rb')).to exist
8
+ end
9
+ end
10
+
11
+ describe '.current_environment' do
12
+ it 'returns "test" when APP_ENV is set to test' do
13
+ expect(described_class.current_environment).to eq('test')
14
+ end
15
+
16
+ it 'defaults to "development" when APP_ENV is not set' do
17
+ allow(ENV).to receive(:fetch).with('APP_ENV', 'development').and_return('development')
18
+ expect(described_class.current_environment).to eq('development')
19
+ end
20
+ end
21
+
22
+ describe '.config_file' do
23
+ it 'returns spec/fixtures path in test environment' do
24
+ path = described_class.config_file
25
+ expect(path.to_s).to include('spec/fixtures/.free_zipcode_data.yml')
26
+ end
27
+ end
28
+
29
+ describe '.os' do
30
+ it 'returns :normal on non-Windows platforms' do
31
+ expect(described_class.os).to eq(:normal)
32
+ end
33
+ end
34
+
35
+ it 'has a version number' do
36
+ expect(FreeZipcodeData::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
37
+ end
38
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,11 +2,32 @@
2
2
 
3
3
  ENV['APP_ENV'] = 'test'
4
4
 
5
- require 'pry'
5
+ begin
6
+ require 'pry'
7
+ rescue NameError, LoadError
8
+ # pry may not be compatible with current Ruby version
9
+ end
10
+
11
+ require 'ostruct'
12
+ require 'free_zipcode_data'
13
+ require 'free_zipcode_data/runner'
6
14
 
7
- Dir[Pathname.new(File.dirname(__FILE__)).parent.join('spec/support/**/*.rb')].sort.each { |f| require f }
15
+ Dir[Pathname.new(File.dirname(__FILE__)).parent.join('spec/support/**/*.rb')].each { |f| require f }
8
16
 
9
17
  RSpec.configure do |config|
18
+ config.include DatabaseHelpers
19
+
20
+ # Silence progress bar and logger output during tests
21
+ config.before do
22
+ allow(ProgressBar).to receive(:create).and_wrap_original do |method, **args|
23
+ method.call(**args, output: StringIO.new)
24
+ end
25
+ FreeZipcodeData::Logger.instance.log_provider = Logger.new(StringIO.new)
26
+ FreeZipcodeData::Options.instance.initialize_hash(
27
+ OpenStruct.new(verbose: false)
28
+ )
29
+ end
30
+
10
31
  config.expect_with :rspec do |expectations|
11
32
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
12
33
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sqlite3'
4
+
5
+ module DatabaseHelpers
6
+ def create_test_database(line_count: 5)
7
+ db = SQLite3::Database.new(':memory:')
8
+ db.execute_batch(<<-SQL)
9
+ CREATE TABLE meta (
10
+ id integer not null primary key,
11
+ name varchar(255),
12
+ value varchar(255)
13
+ )
14
+ SQL
15
+ db.execute("INSERT INTO meta (name, value) VALUES ('line_count', #{line_count})")
16
+ db
17
+ end
18
+
19
+ def seed_countries(db, tablename: 'countries')
20
+ table = FreeZipcodeData::CountryTable.new(database: db, tablename: tablename)
21
+ table.build
22
+ [
23
+ { country: 'US' },
24
+ { country: 'CA' },
25
+ { country: 'GB' }
26
+ ].each { |row| table.write(row) }
27
+ end
28
+
29
+ def seed_states(db, tablename: 'states')
30
+ table = FreeZipcodeData::StateTable.new(database: db, tablename: tablename)
31
+ table.build
32
+ [
33
+ { country: 'US', short_state: 'NY', state: 'New York' },
34
+ { country: 'US', short_state: 'CA', state: 'California' },
35
+ { country: 'US', short_state: 'IL', state: 'Illinois' }
36
+ ].each { |row| table.write(row) }
37
+ end
38
+
39
+ def seed_counties(db, tablename: 'counties')
40
+ table = FreeZipcodeData::CountyTable.new(database: db, tablename: tablename)
41
+ table.build
42
+ [
43
+ { country: 'US', county: 'New York', short_county: '061', short_state: 'NY', state: 'New York' },
44
+ { country: 'US', county: 'Los Angeles', short_county: '037', short_state: 'CA', state: 'California' },
45
+ { country: 'US', county: 'Cook', short_county: '031', short_state: 'IL', state: 'Illinois' }
46
+ ].each { |row| table.write(row) }
47
+ end
48
+ end
metadata CHANGED
@@ -1,80 +1,37 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: free_zipcode_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Blackburn
8
8
  - Chris McKnight
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-09-30 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: bundler
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - ">="
19
- - !ruby/object:Gem::Version
20
- version: '0'
21
- type: :development
22
- prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- version: '0'
28
- - !ruby/object:Gem::Dependency
29
- name: pry-nav
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - "~>"
33
- - !ruby/object:Gem::Version
34
- version: '0.2'
35
- type: :development
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - "~>"
40
- - !ruby/object:Gem::Version
41
- version: '0.2'
42
- - !ruby/object:Gem::Dependency
43
- name: rake
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '13.0'
49
- type: :development
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: '13.0'
56
- - !ruby/object:Gem::Dependency
57
- name: rspec
14
+ name: colored
58
15
  requirement: !ruby/object:Gem::Requirement
59
16
  requirements:
60
17
  - - "~>"
61
18
  - !ruby/object:Gem::Version
62
- version: '3.7'
63
- type: :development
19
+ version: '1.2'
20
+ type: :runtime
64
21
  prerelease: false
65
22
  version_requirements: !ruby/object:Gem::Requirement
66
23
  requirements:
67
24
  - - "~>"
68
25
  - !ruby/object:Gem::Version
69
- version: '3.7'
26
+ version: '1.2'
70
27
  - !ruby/object:Gem::Dependency
71
- name: rubocop
28
+ name: csv
72
29
  requirement: !ruby/object:Gem::Requirement
73
30
  requirements:
74
31
  - - ">="
75
32
  - !ruby/object:Gem::Version
76
33
  version: '0'
77
- type: :development
34
+ type: :runtime
78
35
  prerelease: false
79
36
  version_requirements: !ruby/object:Gem::Requirement
80
37
  requirements:
@@ -82,61 +39,33 @@ dependencies:
82
39
  - !ruby/object:Gem::Version
83
40
  version: '0'
84
41
  - !ruby/object:Gem::Dependency
85
- name: ruby-prof
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - "~>"
89
- - !ruby/object:Gem::Version
90
- version: '0.17'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '0.17'
98
- - !ruby/object:Gem::Dependency
99
- name: simplecov
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - "~>"
103
- - !ruby/object:Gem::Version
104
- version: '0.16'
105
- type: :development
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - "~>"
110
- - !ruby/object:Gem::Version
111
- version: '0.16'
112
- - !ruby/object:Gem::Dependency
113
- name: colored
42
+ name: kiba
114
43
  requirement: !ruby/object:Gem::Requirement
115
44
  requirements:
116
45
  - - "~>"
117
46
  - !ruby/object:Gem::Version
118
- version: '1.2'
47
+ version: '4.0'
119
48
  type: :runtime
120
49
  prerelease: false
121
50
  version_requirements: !ruby/object:Gem::Requirement
122
51
  requirements:
123
52
  - - "~>"
124
53
  - !ruby/object:Gem::Version
125
- version: '1.2'
54
+ version: '4.0'
126
55
  - !ruby/object:Gem::Dependency
127
- name: kiba
56
+ name: logger
128
57
  requirement: !ruby/object:Gem::Requirement
129
58
  requirements:
130
- - - "~>"
59
+ - - ">="
131
60
  - !ruby/object:Gem::Version
132
- version: '4.0'
61
+ version: '0'
133
62
  type: :runtime
134
63
  prerelease: false
135
64
  version_requirements: !ruby/object:Gem::Requirement
136
65
  requirements:
137
- - - "~>"
66
+ - - ">="
138
67
  - !ruby/object:Gem::Version
139
- version: '4.0'
68
+ version: '0'
140
69
  - !ruby/object:Gem::Dependency
141
70
  name: optimist
142
71
  requirement: !ruby/object:Gem::Requirement
@@ -209,6 +138,7 @@ files:
209
138
  - ".rubocop.yml"
210
139
  - ".ruby-version"
211
140
  - CHANGELOG
141
+ - CLAUDE.md
212
142
  - CODE_OF_CONDUCT.md
213
143
  - CONTRIBUTING.md
214
144
  - Gemfile
@@ -242,13 +172,31 @@ files:
242
172
  - lib/free_zipcode_data/version.rb
243
173
  - lib/free_zipcode_data/zipcode_table.rb
244
174
  - lib/tasks/version.rake
175
+ - spec/etl/csv_source_spec.rb
176
+ - spec/etl/free_zipcode_data_job_spec.rb
177
+ - spec/fixtures/.free_zipcode_data.yml
178
+ - spec/fixtures/US.txt
179
+ - spec/fixtures/US.zip
180
+ - spec/fixtures/test_data.csv
181
+ - spec/fixtures/test_data.txt
182
+ - spec/free_zipcode_data/country_table_spec.rb
183
+ - spec/free_zipcode_data/county_table_spec.rb
184
+ - spec/free_zipcode_data/data_source_spec.rb
185
+ - spec/free_zipcode_data/db_table_spec.rb
186
+ - spec/free_zipcode_data/logger_spec.rb
187
+ - spec/free_zipcode_data/options_spec.rb
188
+ - spec/free_zipcode_data/runner_spec.rb
189
+ - spec/free_zipcode_data/sqlite_ram_spec.rb
190
+ - spec/free_zipcode_data/state_table_spec.rb
191
+ - spec/free_zipcode_data/zipcode_table_spec.rb
192
+ - spec/free_zipcode_data_spec.rb
245
193
  - spec/spec_helper.rb
194
+ - spec/support/database_helpers.rb
246
195
  homepage: https://github.com/midwire/free_zipcode_data
247
196
  licenses:
248
197
  - MIT
249
198
  metadata:
250
199
  rubygems_mfa_required: 'true'
251
- post_install_message:
252
200
  rdoc_options: []
253
201
  require_paths:
254
202
  - lib
@@ -256,15 +204,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
256
204
  requirements:
257
205
  - - ">="
258
206
  - !ruby/object:Gem::Version
259
- version: 3.0.2
207
+ version: 3.4.8
260
208
  required_rubygems_version: !ruby/object:Gem::Requirement
261
209
  requirements:
262
210
  - - ">="
263
211
  - !ruby/object:Gem::Version
264
212
  version: '0'
265
213
  requirements: []
266
- rubygems_version: 3.2.22
267
- signing_key:
214
+ rubygems_version: 3.6.9
268
215
  specification_version: 4
269
216
  summary: Free US and world-wide postal codes in SQLite and CSV format
270
217
  test_files: []