civitas 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 28037fc6995f149ecd6fcd77de06bb73314f350543cff25588566d14e4a1b5be
4
+ data.tar.gz: c3491d5efd566a0935fb25fb806b7664e597998d0f2dbdeedd2a39534f9fd3c6
5
+ SHA512:
6
+ metadata.gz: 904e5eb9643e640adfdebcce66fe1e30515c7bd97874dda3dbf387d10c4e6a8b60fa3fbe0bce858770807ff8567e72d6b4cb79672ca60c099a6407ceac337763
7
+ data.tar.gz: 9e77e35ec9febda0aed5284b6acdb4a4550742e05e84f1dbd76ffb993da7635decc31b31b815af6c12b6b1e9a3309e39d65f0960c483894848180b31a9b2ca37
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2023-08-14
9
+
10
+ ### Added
11
+ - Ruby 3 support.
12
+ - Development dependencies: `rspec 3.10`.
13
+ - Unit tests with RSpec.
14
+
15
+ ### Removed
16
+ - Ruby 2.5 support. Minimum Ruby version is now 2.6.0.
17
+ - `Gemfile.lock` from version control.
18
+
19
+ ### Changed
20
+ - Update bundled MaxMind database.
21
+ - Upgrade runtime dependencies: minimum Rake is 11.0, minimum Rubyzip is 2.3.
22
+
23
+ ## [0.1.0] - 2020-03-25
24
+
25
+ ### Added
26
+ - Methods `set_license_key(license_key)` and `set_maxmind_zip_url(url)`.
27
+
28
+ ### Removed
29
+ - Rails-specific code for broader Ruby compatibility.
30
+
31
+ ### Changed
32
+ - Improve methods for renaming and adding missing cities.
33
+ - Update bundled MaxMind database.
34
+ - Upgrade dependencies for security and compatibility.
35
+
36
+ ### Fixed
37
+ - Duplicated city entries. `CS.cities(:CA, :US)` multiple `Burbank` entries.
38
+ - Calling `CS.cities(nil)` returns random values.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2023 Daniel Loureiro
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # Civitas Ruby Gem
2
+
3
+ The `civitas` gem offers a straightforward way to retrieve lists of states for any given country and cities for any state. It's built on the MaxMind database, making it a reliable source for such data.
4
+
5
+ ## Compatibility
6
+
7
+ This branch (`main`) is compatible with **Ruby 3 and higher**. If you are using Ruby 2, please refer to the `v0` branch.
8
+
9
+ ### Ruby 3 and Higher
10
+
11
+ - This is the primary development branch.
12
+ - New features and improvements will be added here.
13
+ - Ensure you have Ruby 3 or higher to use the versions from this branch.
14
+
15
+ ### Ruby 2 Support
16
+
17
+ - For Ruby 2 support, please refer to the `v0` branch.
18
+ - The `v0` branch is in maintenance mode, which means it will only receive bug fixes and will not get any new features.
19
+
20
+ ## Installation
21
+
22
+ Add the gem to your Gemfile:
23
+
24
+ ```ruby
25
+ gem 'civitas'
26
+ ```
27
+
28
+ Then, run:
29
+
30
+ ```bash
31
+ $ bundle install
32
+ ```
33
+
34
+ ## Listing States:
35
+
36
+ Retrieve a list of states for a specified country:
37
+
38
+ ```ruby
39
+ CS.states(:US)
40
+ ```
41
+ **Note:** The gem is case-insensitive. You can use variations like `:US`, `:us`, `:Us`, `"us"`, and `"US"`.
42
+
43
+ ## Listing Cities:
44
+
45
+ Retrieve a list of cities for a specified state and country:
46
+
47
+ ```ruby
48
+ CS.cities(:AK, :US)
49
+ ```
50
+
51
+ You can also specify the country, though it's optional. The gem remembers the last country you used:
52
+
53
+ ```ruby
54
+ CS.states(:BR)
55
+
56
+ CS.cities(:TO) # This will use Brazil (BR) as the country
57
+ ```
58
+
59
+ Miscellaneous Notes:
60
+ - The country is an optional argument. The gem always uses the last country that you used.
61
+
62
+ ## Listing Countries:
63
+
64
+ ```ruby
65
+ CS.countries
66
+ ```
67
+
68
+ ## Missing cities and wrong names
69
+ To add missing cities or to rename wrong ones, create these files in your project folder:
70
+ `db/cities-lookup.yml` and `db/states-lookup.yml` and `db/countries-lookup.yml`:
71
+
72
+ ### Renaming a country - `US` to `America`:
73
+
74
+ ```yaml
75
+ # db/countries-lookup.yml
76
+ US: "America"
77
+ ```
78
+
79
+ ### Renaming a state - `California` to `Something Else`:
80
+
81
+ ```yaml
82
+ # db/states-lookup.yml
83
+ US:
84
+ CA: Something Else
85
+ ```
86
+
87
+ ### Renaming a city:
88
+
89
+ ```yaml
90
+ # db/cities-lookup.yml
91
+ US:
92
+ CA:
93
+ "Burbank": "Bur Bank"
94
+ ```
95
+
96
+ ### Adding a missing city:
97
+
98
+ ```yaml
99
+ # db/cities-lookup.yml
100
+ US:
101
+ CA:
102
+ "My Town": "My Town"
103
+ ```
104
+
105
+ ### Suppressing a city (set it as a blank line):
106
+ ```yaml
107
+ # db/cities-lookup.yml
108
+ US:
109
+ CA:
110
+ "Burbank": ""
111
+ ```
112
+
113
+ ### To use a different file instead of `db\cities-lookup.yml`:
114
+
115
+ ```ruby
116
+ CS.set_cities_lookup_file('new-city-names.yml')
117
+
118
+ CS.set_states_lookup_file('new-state-names.yml')
119
+
120
+ CS.set_countries_lookup_file('new-country-names.yml')
121
+ ```
122
+
123
+ ## Updating MaxMind database
124
+ MaxMind update their databases weekly on Tuesdays.
125
+
126
+ Since Dec 30, 2019, MaxMind requires a license key (for free) to get download updates.
127
+
128
+ To get the license key:
129
+ 1. Sign up for a MaxMind account: https://www.maxmind.com/en/geolite2/signup
130
+ 2. Create a license key: https://www.maxmind.com/en/accounts/current/license-key
131
+ 3. There's no need to download anything.
132
+
133
+ To update:
134
+
135
+ ```ruby
136
+ CS.set_license_key('MY_KEY')
137
+
138
+ CS.update
139
+ ```
140
+ **Note:** Replace `MY_KEY` with your actual license key.
141
+
142
+ ## Manually setting a database file:
143
+
144
+ You can use an alternative database file instead of downloading from MaxMind servers:
145
+
146
+ ```ruby
147
+ CS.set_maxmind_zip_url('/home/daniel/GeoLite2-City-CSV_20200324.zip')
148
+
149
+ CS.update
150
+ ```
151
+
152
+ or
153
+
154
+ ```ruby
155
+ CS.set_maxmind_zip_url('https://example.com/GeoLite2-City-CSV_20200324.zip')
156
+
157
+ CS.update
158
+ ```
159
+
160
+ The file has to be a ZIP file. And it has to contain a CVS file named `GeoLite2-City-Locations-en.csv`. This file must be in MaxMind's GeoLite2 City's format.
161
+
162
+ ## Changelog
163
+ See [CHANGELOG.md](CHANGELOG.md)
164
+
165
+ ## How the original `city-state` gem was created
166
+ https://learnwithdaniel.com/2015/02/citystate-list-of-countries-cities-and-states-ruby/
167
+
168
+ ## civitas License
169
+ **civitas** is a open source project forked from `city-state` by Daniel Loureiro with a MIT license. Also, it uses MaxMind open source database.
170
+
171
+ ## MaxMind License
172
+ Database and Contents Copyright (c) 2020 MaxMind, Inc.
173
+ This work is licensed under the Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/civitas.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'civitas/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "civitas"
8
+ spec.version = CS::VERSION
9
+ spec.authors = ["Daniel Loureiro"]
10
+ spec.email = ["loureirorg@gmail.com"]
11
+ spec.summary = %q{Simple list of cities and states of the world}
12
+ spec.description = %q{Useful to make forms and validations. It uses MaxMind database.}
13
+ spec.homepage = "https://github.com/duduribeiro/civitas"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = [
17
+ 'Rakefile',
18
+ 'README.md',
19
+ 'LICENSE.txt',
20
+ 'CHANGELOG.md',
21
+ 'civitas.gemspec'
22
+ ] + Dir["lib/**/*"] + Dir["spec/**/*"]
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.required_ruby_version = '>= 2.6.0'
26
+
27
+ spec.add_runtime_dependency "rubyzip", ">= 2.3"
28
+
29
+ spec.add_development_dependency "bundler", ">= 1.7"
30
+ spec.add_development_dependency "rake", ">= 11.0"
31
+ spec.add_development_dependency 'rspec', '~> 3.10'
32
+ end
@@ -0,0 +1,3 @@
1
+ module CS
2
+ VERSION = "1.1.1"
3
+ end
data/lib/civitas.rb ADDED
@@ -0,0 +1,353 @@
1
+ require 'uri'
2
+ require "civitas/version"
3
+ require 'yaml'
4
+
5
+ module CS
6
+ # CS constants
7
+ FILES_FOLDER = File.expand_path('../db', __FILE__)
8
+ MAXMIND_DB_FN = File.join(FILES_FOLDER, "GeoLite2-City-Locations-en.csv")
9
+ COUNTRIES_FN = File.join(FILES_FOLDER, "countries.yml")
10
+ DEFAULT_CITIES_LOOKUP_FN = 'db/cities-lookup.yml'
11
+ DEFAULT_STATES_LOOKUP_FN = 'db/states-lookup.yml'
12
+ DEFAULT_COUNTRIES_LOOKUP_FN = 'db/countries-lookup.yml'
13
+
14
+ @countries, @states, @cities = [{}, {}, {}]
15
+ @current_country = nil # :US, :BR, :GB, :JP, ...
16
+ @maxmind_zip_url = nil
17
+ @license_key = nil
18
+
19
+ # lookup tables for state/cities renaming
20
+ @cities_lookup_fn = nil
21
+ @cities_lookup = nil
22
+ @states_lookup_fn = nil
23
+ @states_lookup = nil
24
+ @countries_lookup_fn = nil
25
+ @countries_lookup = nil
26
+
27
+ def self.set_maxmind_zip_url(maxmind_zip_url)
28
+ @maxmind_zip_url = maxmind_zip_url
29
+ end
30
+
31
+ def self.set_license_key(license_key)
32
+ url = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&license_key=#{license_key}&suffix=zip"
33
+ @license_key = license_key
34
+ self.set_maxmind_zip_url(url)
35
+ end
36
+
37
+ def self.update_maxmind
38
+ require "open-uri"
39
+ require "zip"
40
+
41
+ # get zipped file
42
+ return false if !@maxmind_zip_url
43
+ f_zipped = URI.open(@maxmind_zip_url)
44
+
45
+ # unzip file:
46
+ # recursively searches for "GeoLite2-City-Locations-en"
47
+ Zip::File.open(f_zipped) do |zip_file|
48
+ zip_file.each do |entry|
49
+ if self.present?(entry.name["GeoLite2-City-Locations-en"])
50
+ fn = entry.name.split("/").last
51
+ entry.extract(File.join(FILES_FOLDER, fn)) { true } # { true } is to overwrite
52
+ break
53
+ end
54
+ end
55
+ end
56
+ true
57
+ end
58
+
59
+ def self.update
60
+ self.update_maxmind # update via internet
61
+ Dir[File.join(FILES_FOLDER, "states.*")].each do |state_fn|
62
+ self.install(state_fn.split(".").last.upcase.to_sym) # reinstall country
63
+ end
64
+ @countries, @states, @cities = [{}, {}, {}] # invalidades cache
65
+ File.delete COUNTRIES_FN # force countries.yml to be generated at next call of CS.countries
66
+ true
67
+ end
68
+
69
+ # constants: CVS position
70
+ ID = 0
71
+ COUNTRY = 4
72
+ COUNTRY_LONG = 5
73
+ STATE = 6
74
+ STATE_LONG = 7
75
+ CITY = 10
76
+
77
+ def self.install(country)
78
+ # get CSV if doesn't exists
79
+ update_maxmind unless File.exist? MAXMIND_DB_FN
80
+
81
+ # normalize "country"
82
+ country = country.to_s.upcase
83
+
84
+ # some state codes are empty: we'll use "states-replace" in these cases
85
+ states_replace_fn = File.join(FILES_FOLDER, "states-replace.yml")
86
+ states_replace = self.symbolize_keys(YAML::load_file(states_replace_fn))
87
+ states_replace = states_replace[country.to_sym] || {} # we need just this country
88
+ states_replace_inv = states_replace.invert # invert key with value, to ease the search
89
+
90
+ # read CSV line by line
91
+ cities = {}
92
+ states = {}
93
+ File.foreach(MAXMIND_DB_FN) do |line|
94
+ rec = line.split(",")
95
+ next if rec[COUNTRY] != country
96
+ next if (self.blank?(rec[STATE]) && self.blank?(rec[STATE_LONG])) || self.blank?(rec[CITY])
97
+
98
+ # some state codes are empty: we'll use "states-replace" in these cases
99
+ rec[STATE] = states_replace_inv[rec[STATE_LONG]] if self.blank?(rec[STATE])
100
+ rec[STATE] = rec[STATE_LONG] if self.blank?(rec[STATE]) # there's no correspondent in states-replace: we'll use the long name as code
101
+
102
+ # some long names are empty: we'll use "states-replace" to get the code
103
+ rec[STATE_LONG] = states_replace[rec[STATE]] if self.blank?(rec[STATE_LONG])
104
+
105
+ # normalize
106
+ rec[STATE] = rec[STATE].to_sym
107
+ rec[CITY].gsub!(/\"/, "") # sometimes names come with a "\" char
108
+ rec[STATE_LONG].gsub!(/\"/, "") # sometimes names come with a "\" char
109
+
110
+ # cities list: {TX: ["Texas City", "Another", "Another 2"]}
111
+ cities.merge!({rec[STATE] => []}) if ! states.has_key?(rec[STATE])
112
+ cities[rec[STATE]] << rec[CITY]
113
+
114
+ # states list: {TX: "Texas", CA: "California"}
115
+ if ! states.has_key?(rec[STATE])
116
+ state = {rec[STATE] => rec[STATE_LONG]}
117
+ states.merge!(state)
118
+ end
119
+ end
120
+
121
+ # sort
122
+ cities = Hash[cities.sort]
123
+ states = Hash[states.sort]
124
+ cities.each { |k, v| cities[k].sort! }
125
+
126
+ # save to states.us and cities.us
127
+ states_fn = File.join(FILES_FOLDER, "states.#{country.downcase}")
128
+ cities_fn = File.join(FILES_FOLDER, "cities.#{country.downcase}")
129
+ File.open(states_fn, "w") { |f| f.write states.to_yaml }
130
+ File.open(cities_fn, "w") { |f| f.write cities.to_yaml }
131
+ File.chmod(0666, states_fn, cities_fn) # force permissions to rw_rw_rw_ (issue #3)
132
+ true
133
+ end
134
+
135
+ def self.current_country
136
+ return @current_country if self.present?(@current_country)
137
+
138
+ # we don't have used this method yet: discover by the file extension
139
+ fn = Dir[File.join(FILES_FOLDER, "cities.*")].last
140
+ @current_country = self.blank?(fn) ? nil : fn.split(".").last
141
+
142
+ # there's no files: we'll install and use :US
143
+ if self.blank?(@current_country)
144
+ @current_country = :US
145
+ self.install(@current_country)
146
+
147
+ # we find a file: normalize the extension to something like :US
148
+ else
149
+ @current_country = @current_country.to_s.upcase.to_sym
150
+ end
151
+
152
+ @current_country
153
+ end
154
+
155
+ def self.current_country=(country)
156
+ @current_country = country.to_s.upcase.to_sym
157
+ end
158
+
159
+ def self.cities(state, country = nil)
160
+ self.current_country = country if self.present?(country) # set as current_country
161
+ country = self.current_country
162
+ state = state.to_s.upcase.to_sym
163
+
164
+ # load the country file
165
+ if self.blank?(@cities[country])
166
+ cities_fn = File.join(FILES_FOLDER, "cities.#{country.to_s.downcase}")
167
+ self.install(country) if ! File.exist? cities_fn
168
+ @cities[country] = self.symbolize_keys(YAML::load_file(cities_fn))
169
+
170
+ # Remove duplicated cities
171
+ @cities[country].each do |key, value|
172
+ @cities[country][key] = value.uniq || []
173
+ end
174
+
175
+ # Process lookup table
176
+ lookup = get_cities_lookup(country)
177
+ if ! lookup.nil?
178
+ lookup.each do |state, new_values|
179
+ new_values.each do |old_value, new_value|
180
+ if new_value.nil? || self.blank?(new_value)
181
+ @cities[country][state].delete(old_value)
182
+ else
183
+ index = @cities[country][state].index(old_value)
184
+ if index.nil?
185
+ @cities[country][state] << new_value
186
+ else
187
+ @cities[country][state][index] = new_value
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ @cities[country][state] = @cities[country][state].sort # sort it alphabetically
194
+ end
195
+ end
196
+
197
+ # Return list
198
+ @cities[country][state]
199
+ end
200
+
201
+ def self.set_cities_lookup_file(filename)
202
+ @cities_lookup_fn = filename
203
+ @cities_lookup = nil
204
+ end
205
+
206
+ def self.set_states_lookup_file(filename)
207
+ @states_lookup_fn = filename
208
+ @states_lookup = nil
209
+ end
210
+
211
+ def self.set_countries_lookup_file(filename)
212
+ @countries_lookup_fn = filename
213
+ @countries_lookup = nil
214
+ end
215
+
216
+ def self.get_cities_lookup(country)
217
+ # lookup file not loaded
218
+ if @cities_lookup.nil?
219
+ @cities_lookup_fn = DEFAULT_CITIES_LOOKUP_FN if @cities_lookup_fn.nil?
220
+ @cities_lookup_fn = File.expand_path(@cities_lookup_fn)
221
+ return nil if ! File.exist?(@cities_lookup_fn)
222
+ @cities_lookup = self.symbolize_keys(YAML::load_file(@cities_lookup_fn)) # force countries to be symbols
223
+ @cities_lookup.each { |key, value| @cities_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
224
+ end
225
+
226
+ return nil if ! @cities_lookup.key?(country)
227
+ @cities_lookup[country]
228
+ end
229
+
230
+ def self.get_states_lookup(country)
231
+ # lookup file not loaded
232
+ if @states_lookup.nil?
233
+ @states_lookup_fn = DEFAULT_STATES_LOOKUP_FN if @states_lookup_fn.nil?
234
+ @states_lookup_fn = File.expand_path(@states_lookup_fn)
235
+ return nil if ! File.exist?(@states_lookup_fn)
236
+ @states_lookup = self.symbolize_keys(YAML::load_file(@states_lookup_fn)) # force countries to be symbols
237
+ @states_lookup.each { |key, value| @states_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
238
+ end
239
+
240
+ return nil if ! @states_lookup.key?(country)
241
+ @states_lookup[country]
242
+ end
243
+
244
+ def self.get_countries_lookup
245
+ # lookup file not loaded
246
+ if @countries_lookup.nil?
247
+ @countries_lookup_fn = DEFAULT_COUNTRIES_LOOKUP_FN if @countries_lookup_fn.nil?
248
+ @countries_lookup_fn = File.expand_path(@countries_lookup_fn)
249
+ return nil if ! File.exist?(@countries_lookup_fn)
250
+ @countries_lookup = self.symbolize_keys(YAML::load_file(@countries_lookup_fn)) # force countries to be symbols
251
+ end
252
+
253
+ @countries_lookup
254
+ end
255
+
256
+ def self.states(country)
257
+ # Bugfix: https://github.com/loureirorg/city-state/issues/24
258
+ return {} if country.nil?
259
+
260
+ # Set it as current_country
261
+ self.current_country = country # set as current_country
262
+ country = self.current_country # normalized
263
+
264
+ # Load the country file
265
+ if self.blank?(@states[country])
266
+ states_fn = File.join(FILES_FOLDER, "states.#{country.to_s.downcase}")
267
+ self.install(country) if ! File.exist? states_fn
268
+ @states[country] = self.symbolize_keys(YAML::load_file(states_fn))
269
+
270
+ # Process lookup table
271
+ lookup = get_states_lookup(country)
272
+ if ! lookup.nil?
273
+ lookup.each do |key, value|
274
+ if value.nil? || self.blank?(value)
275
+ @states[country].delete(key)
276
+ else
277
+ @states[country][key] = value
278
+ end
279
+ end
280
+ @states[country] = @states[country].sort.to_h # sort it alphabetically
281
+ end
282
+ end
283
+
284
+ # Return list
285
+ @states[country] || {}
286
+ end
287
+
288
+ # list of all countries of the world (countries.yml)
289
+ def self.countries
290
+ if ! File.exist? COUNTRIES_FN
291
+ # countries.yml doesn't exists, extract from MAXMIND_DB
292
+ update_maxmind unless File.exist? MAXMIND_DB_FN
293
+
294
+ # reads CSV line by line
295
+ File.foreach(MAXMIND_DB_FN) do |line|
296
+ rec = line.split(",")
297
+ next if self.blank?(rec[COUNTRY]) || self.blank?(rec[COUNTRY_LONG]) # jump empty records
298
+ country = rec[COUNTRY].to_s.upcase.to_sym # normalize to something like :US, :BR
299
+ if self.blank?(@countries[country])
300
+ long = rec[COUNTRY_LONG].gsub(/\"/, "") # sometimes names come with a "\" char
301
+ @countries[country] = long
302
+ end
303
+ end
304
+
305
+ # sort and save to "countries.yml"
306
+ @countries = Hash[@countries.sort]
307
+ File.open(COUNTRIES_FN, "w") { |f| f.write @countries.to_yaml }
308
+ File.chmod(0666, COUNTRIES_FN) # force permissions to rw_rw_rw_ (issue #3)
309
+ else
310
+ # countries.yml exists, just read it
311
+ @countries = self.symbolize_keys(YAML::load_file(COUNTRIES_FN))
312
+ end
313
+
314
+ # Applies `countries-lookup.yml` if exists
315
+ lookup = self.get_countries_lookup()
316
+ if ! lookup.nil?
317
+ lookup.each do |key, value|
318
+ if value.nil? || self.blank?(value)
319
+ @countries.delete(key)
320
+ else
321
+ @countries[key] = value
322
+ end
323
+ end
324
+ @countries = @countries.sort.to_h # sort it alphabetically
325
+ end
326
+
327
+ # Return countries list
328
+ @countries
329
+ end
330
+
331
+ # get is a method to simplify the use of city-state
332
+ # get = countries, get(country) = states(country), get(country, state) = cities(state, country)
333
+ def self.get(country = nil, state = nil)
334
+ return self.countries if country.nil?
335
+ return self.states(country) if state.nil?
336
+ return self.cities(state, country)
337
+ end
338
+
339
+ # Emulates Rails' `blank?` method
340
+ def self.blank?(obj)
341
+ obj.respond_to?(:empty?) ? !!obj.empty? : !obj
342
+ end
343
+
344
+ # Emulates Rails' `present?` method
345
+ def self.present?(obj)
346
+ !self.blank?(obj)
347
+ end
348
+
349
+ # Emulates Rails' `symbolize_keys` method
350
+ def self.symbolize_keys(obj)
351
+ obj.transform_keys { |key| key.to_sym rescue key }
352
+ end
353
+ end