geopolitical 4.1.1 → 4.2.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: 7c83de7fa358edf3032e002afa160eb629022a374f589f89a4e1c061d4113586
4
- data.tar.gz: e262aefcfd984280b32c4aeddea8f64fd600fd130a303f10dc7631dbaddfb167
3
+ metadata.gz: d9d53d656a3190699410d4bdd673d9cb59ebc8ff25c9bca39bf7bde033f28e31
4
+ data.tar.gz: 14c19318010902ce4988843a63333a71dc1cb2596524a84a13208ea2c21dba78
5
5
  SHA512:
6
- metadata.gz: 15b0754ffb716100a94ef4f539e605de42a543e17144c0a200187468fb9bce6855b4be25f0b62789ae0b3a291543ee190bf67f015d9d5ee94fdc45d77e3b0951
7
- data.tar.gz: ed2410dfb3547275426197641976c1c1b9a7a0c54e687465e4ea3bc79fd9f5692b6ce68aefa8ac83024b61b838833aa70f76ff0b351cce6eb962e79eb950089e
6
+ metadata.gz: 38cb57c04db412f24dc8d0843b93fcc2e96f022c58ae3d9973b95c1f1add9d461070a728b2091ac3e114e7d7a23a7d53bb512c2d1d42944a427927f9d1f0ca1f
7
+ data.tar.gz: b4d3a57a2762bdb5ea9b5abb635efa90346d91165f742487987fb6c56838fcb4341e7ffb42de298ae83ff9b1b6555583244379fe7e29e53de1d9b38211b9e601
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.2.0
4
+
5
+ - `City` includes `Mongoid::Geospatial::Geom` — the pin is the gem's, `within(geom, km)` too.
6
+ - Indexes match the queries (README § The models). Gone, nothing read them: `name_1` on all four (`name` is localized, Mongoid asks `name.<locale>`), `cities.name_1_nation_id_1`, `cities.nation_id_1` (the unique compound leads with it), `regions.abbr_1` (every abbr lookup carries its nation), `code_1` on nations, regions and hoods (only City's is read, and stays). Hosts: `DROP_INDEXES=1` on the next deploy sheds them.
7
+ - `Nation` checks a taken `abbr` as an equality on `abbr_1` (its writer upcases), not `/\ABR\z/i`.
8
+ - `City` and `Hood` validate `slug` uniqueness: a taken slug is a 422 with its message, not an E11000 (a 500 in any form). Slugs are unique within a nation, not across: BR and AR both have a BA.
9
+ - Every uniqueness message lives in the locales, en and pt (`mongoid.errors.models.<model>.attributes.<field>.taken`), no English literal in the models.
10
+ - `Geopolitical::Postal.city` finds a slug in the address's own nation only: an Argentine "Salvador" in BA got Bahia's.
11
+ - `.search` matches `\A`, not `^`: bson sends a Ruby regexp multiline, so `^` scanned all of `slug_1`. Now it reads the prefix's keys only.
12
+
3
13
  ## 4.1.0
4
14
 
5
15
  - Engine views are ERB; the `haml` gem is gone.
data/README.md CHANGED
@@ -112,7 +112,9 @@ Region.ordered # by name
112
112
  ### Addresses — `Geopolitical::Postal`
113
113
 
114
114
  Two ways in, one answer out: a `Found` with the street, number, hood, postal
115
- code and the `City` (found, or made under its state's `Region`).
115
+ code and the `City` (found, or made under its state's `Region`). Found in
116
+ the address's own nation only: Brazil and Argentina both have a `BA`, and an
117
+ Argentine "Salvador" never gets Bahia's `salvador-ba`.
116
118
 
117
119
  ```
118
120
  typing "av paulista 1578" ──suggest──► [Suggestion(ref, text), …] free, nothing kept
@@ -179,6 +181,26 @@ Every model shares (via the `Geopolitocracy` concern):
179
181
  `name` & `alt` (localized), `abbr`, `nick`, `ascii`, `code`, `slug`,
180
182
  `souls`/`population`, `postal`, `phone`, `.search`, `.ordered`, `to_s`, `==`, `<=>`.
181
183
 
184
+ The indexes, and the query each one is for (`Model.create_indexes` builds them):
185
+
186
+ ```
187
+ nations slug (u) · abbr (u) Nation['br'] is the _id
188
+ regions nation+name (u) · nation+abbr (u, when abbr) · nation+slug (u) · slug
189
+ cities slug (u) · nation+region+name (u) · region · souls · code · geom 2dsphere
190
+ hoods city+name (u) · slug (u)
191
+ ```
192
+
193
+ A lookup by nation rides the unique compound it leads; a Region's abbr is
194
+ always asked with its nation. `.search` is a slug prefix, `\A`, never `^`:
195
+ bson sends a Ruby regexp multiline, and `^` there bounds no index.
196
+
197
+ `name` is localized, so Mongoid asks for `name.<locale>` (`where`, `order_by`,
198
+ the uniqueness check) and no index on the bare `name` hash could answer it.
199
+ Only the unique ones key it: they referee two creates of the same name. A
200
+ uniqueness the model validates is a unique index here, and a unique index
201
+ here is a validation there: a clash is a 422 with its message, never an
202
+ E11000.
203
+
182
204
  Names get titleized only when you shout or mumble: `'new york'` and
183
205
  `'NEW YORK'` become `"New York"`; `'McAllen'` stays `"McAllen"`.
184
206
 
data/app/models/city.rb CHANGED
@@ -10,17 +10,14 @@
10
10
  # abbreviation to ensure uniqueness across regions.
11
11
  class City
12
12
  include Mongoid::Document
13
- include Mongoid::Geospatial # For GIS capabilities like `geom` field and spatial queries
13
+ include Mongoid::Geospatial::Geom
14
14
  include Geopolitocracy
15
15
 
16
16
  # @!attribute [rw] area
17
17
  # @return [Integer] The square area of the city in square meters (m2).
18
18
  field :area, type: Integer
19
19
 
20
- # @!attribute [rw] geom
21
- # @return [Point] [lng, lat], 2dsphere — `nearby` is `$nearSphere`.
22
- #
23
- field :geom, type: Point, sphere: true
20
+ # geom — [lng, lat], 2dsphere. `City.nearby` / `City.within(point, km)`.
24
21
 
25
22
  # @!attribute [rw] rbbr
26
23
  # @return [String] Cached abbreviation of the city's region.
@@ -50,7 +47,8 @@ class City
50
47
 
51
48
  before_validation :ensure_derived_fields_and_slug
52
49
 
53
- validates :name, uniqueness: { scope: %i[nation_id region_id], message: 'must be unique within its region' }
50
+ validates :name, uniqueness: { scope: %i[nation_id region_id] }
51
+ validates :slug, uniqueness: true # slug_1 refuses a twin anyway; this says so before the E11000
54
52
  validates :nation, presence: true
55
53
  validate :region_inside_nation_if_region_present
56
54
 
@@ -59,11 +57,11 @@ class City
59
57
  scope :population, -> { order_by(souls: :desc) }
60
58
 
61
59
  index({ slug: 1 }, unique: true)
62
- index({ name: 1, nation_id: 1 }) # For lookups by name within a nation
63
- index({ nation_id: 1, region_id: 1, name: 1 }, { unique: true }) # Backs the name uniqueness validation
64
- index({ nation_id: 1 }) # For finding all cities in a nation
60
+ # Backs the name uniqueness validation; its nation_id prefix is every lookup by nation.
61
+ index({ nation_id: 1, region_id: 1, name: 1 }, { unique: true })
65
62
  index({ region_id: 1 }, sparse: true) # For finding cities in a region, sparse if region is optional
66
63
  index({ souls: -1 }) # For sorting by population count
64
+ index({ code: 1 }, sparse: true) # The IBGE code: Geopolitical::Postal.city, fiscal documents
67
65
 
68
66
  # Validates that if a city is associated with a region, that region
69
67
  # belongs to the same nation as the city.
@@ -111,8 +109,10 @@ class City
111
109
  self.slug = name
112
110
  end
113
111
 
114
- # 3. Suffix the slug so it is unique planet-wide: the region abbr where there
115
- # is a region ("santos-sp"), the nation abbr otherwise ("singapore-sg").
112
+ # 3. Suffix the slug: the region abbr where there is a region ("santos-sp"),
113
+ # the nation abbr otherwise ("singapore-sg"). Unique within a nation, not
114
+ # across them: BR and AR both have a BA, so a second "salvador-ba" is a
115
+ # taken slug (the validation's message), never a silent twin.
116
116
  suffix = region_abbr.presence || nation&.abbr
117
117
  return if suffix.blank? || slug.blank?
118
118
 
@@ -76,13 +76,11 @@ module Geopolitocracy
76
76
  validates :slug, presence: true # Uniqueness will be handled by including models with appropriate scope
77
77
  # @!endgroup
78
78
 
79
- # @!group Indexes
80
- # NOTE: no slug or abbr index here — each model declares its own (unique where
81
- # it can be). Mongoid keys a declaration on the fields alone, so a second
82
- # `index({ abbr: 1 }, unique: true)` in a model would be silently ignored.
83
- index({ name: 1 }) # For sorting and lookups by name
84
- index({ code: 1 }, { sparse: true }) # Sparse as code can be nil
85
- # @!endgroup
79
+ # No index here: each model declares the ones its queries read (README
80
+ # § The models). Mongoid keys a declaration on the fields alone, so one
81
+ # here would silently win over a model's own on the same key. `name` is
82
+ # localized: Mongoid asks name.<locale>, and no index on the bare hash
83
+ # answers that.
86
84
 
87
85
  before_validation :ensure_slug_is_generated
88
86
 
@@ -140,7 +138,7 @@ module Geopolitocracy
140
138
  if exact # matches the exact slug
141
139
  where(slug: parameterized_query)
142
140
  else
143
- where(slug: /^#{Regexp.escape(parameterized_query)}/)
141
+ where(slug: /\A#{Regexp.escape(parameterized_query)}/) # \A: bson sends ^ multiline, unbounded
144
142
  end
145
143
  end
146
144
  # @!endgroup
@@ -77,9 +77,13 @@ module Geopolitical
77
77
  return if region.nil? || name.blank?
78
78
 
79
79
  slug = "#{Geopolitocracy.slugify(name)}-#{Geopolitocracy.slugify(region.abbr)}"
80
- City.where(slug: slug).first || make_city(name, code, region) || City.where(slug: slug).first
80
+ town(region, slug) || make_city(name, code, region) || town(region, slug)
81
81
  end
82
82
 
83
+ # A slug is unique within a nation, not across (BR and AR both have a
84
+ # BA): never another nation's town.
85
+ def town(region, slug) = City.where(nation_id: region.nation_id, slug: slug).first
86
+
83
87
  def region(nation, state)
84
88
  country = Nation[nation] or return
85
89
  Region.where(nation_id: country.id, abbr: state.to_s.upcase).first
data/app/models/hood.rb CHANGED
@@ -18,14 +18,11 @@ class Hood
18
18
  belongs_to :city
19
19
 
20
20
  validates :city, presence: true
21
- validates :name, uniqueness: { scope: :city_id, message: 'must be unique within its city' }
22
- # `abbr` (from Geopolitocracy) could also be validated for uniqueness within the city if used.
23
- # validates :abbr,
24
- # uniqueness: { scope: :city_id, allow_nil: true, message: "must be unique within its city if provided" }
21
+ validates :name, uniqueness: { scope: :city_id }
22
+ validates :slug, uniqueness: true # slug_1 refuses a twin anyway; this says so before the E11000
25
23
 
26
24
  index({ city_id: 1, name: 1 }, { unique: true })
27
- # index({ city_id: 1, abbr: 1 }, { unique: true, sparse: true }) # If abbr is used and needs to be unique
28
- index({ slug: 1 }, { unique: true }) # Hood slugs are globally unique due to city_slug prefix
25
+ index({ slug: 1 }, { unique: true }) # The city slug prefixes it, so two names that slugify alike collide
29
26
 
30
27
  before_validation :ensure_slug_for_hood
31
28
 
data/app/models/nation.rb CHANGED
@@ -45,8 +45,8 @@ class Nation
45
45
  # alias iso_3166_3= code3=
46
46
 
47
47
  validates :abbr, presence: true # Name presence is validated in Geopolitocracy
48
- validates_uniqueness_of :abbr, case_sensitive: false
49
- validates :slug, uniqueness: { message: 'must be unique' } # Slug presence is validated in Geopolitocracy
48
+ validates_uniqueness_of :abbr # an equality: abbr= upcases, so the case is already one
49
+ validates :slug, uniqueness: true # Slug presence is validated in Geopolitocracy
50
50
 
51
51
  # @!attribute [rw] capital
52
52
  # @return [City] The capital city of this nation.
@@ -59,7 +59,6 @@ class Nation
59
59
  # @return [Mongoid::Relations::Targets::Enumerable<City>] The cities within this nation.
60
60
  has_many :cities, dependent: :destroy
61
61
 
62
- index({ name: 1 }) # Index for sorting by name, common operation
63
62
  index({ slug: 1 }, { unique: true }) # Slugs must be globally unique for nations
64
63
  index({ abbr: 1 }, { unique: true }) # abbr is upcased by its writer, so plain unique is case-insensitive
65
64
 
data/app/models/region.rb CHANGED
@@ -27,18 +27,17 @@ class Region
27
27
  belongs_to :capital, inverse_of: :region_governancy, class_name: 'City', optional: true
28
28
 
29
29
  validates :nation, presence: true
30
- validates :name, uniqueness: { scope: :nation_id, message: 'must be unique within its nation' }
31
- # `abbr` (from Geopolitocracy) should also be unique within its nation if present.
32
- validates :abbr,
33
- uniqueness: { scope: :nation_id, allow_blank: true, message: 'must be unique within its nation if provided' }
34
- # Slug (from Geopolitocracy) should be unique within its nation. Presence is already validated by Geopolitocracy.
35
- validates :slug, uniqueness: { scope: :nation_id, message: 'must be unique within its nation' }
30
+ # Each message is mongoid.errors.models.region.attributes.<field>.taken (en, pt).
31
+ validates :name, uniqueness: { scope: :nation_id }
32
+ validates :abbr, uniqueness: { scope: :nation_id, allow_blank: true }
33
+ validates :slug, uniqueness: { scope: :nation_id }
36
34
 
37
35
  index({ slug: 1 }) # For .search; global uniqueness is not required, see the compound index below
38
- index({ abbr: 1 }, { sparse: true }) # Sparse index as abbr can be nil
39
36
  index({ nation_id: 1, name: 1 }, { unique: true }) # Enforce uniqueness of name within nation
40
37
  # Partial, not sparse: nation_id is always set, so a sparse compound index
41
- # still indexes every abbr-less region and the second one collides.
38
+ # still indexes every abbr-less region and the second one collides. Every
39
+ # abbr lookup carries its nation (an abbr alone is ambiguous: BR and AR both
40
+ # have a BA), so this is the abbr index too, `$in` included.
42
41
  index({ nation_id: 1, abbr: 1 }, { unique: true, partial_filter_expression: { abbr: { '$gt' => '' } } })
43
42
  index({ nation_id: 1, slug: 1 }, { unique: true }) # Enforce uniqueness of slug within nation
44
43
 
@@ -56,6 +56,32 @@ en:
56
56
  souls: Population
57
57
  postal: Postal
58
58
  phone: Phone
59
+ errors:
60
+ models:
61
+ nation:
62
+ attributes:
63
+ slug:
64
+ taken: must be unique
65
+ region:
66
+ attributes:
67
+ name:
68
+ taken: must be unique within its nation
69
+ abbr:
70
+ taken: must be unique within its nation if provided
71
+ slug:
72
+ taken: must be unique within its nation
73
+ city:
74
+ attributes:
75
+ name:
76
+ taken: must be unique within its region
77
+ slug:
78
+ taken: is taken by another city
79
+ hood:
80
+ attributes:
81
+ name:
82
+ taken: must be unique within its city
83
+ slug:
84
+ taken: is taken by another hood
59
85
  geopolitical:
60
86
  title: Geopolitical
61
87
  actions:
@@ -56,6 +56,32 @@ pt:
56
56
  souls: População
57
57
  postal: CEP
58
58
  phone: Telefone
59
+ errors:
60
+ models:
61
+ nation:
62
+ attributes:
63
+ slug:
64
+ taken: já está em uso
65
+ region:
66
+ attributes:
67
+ name:
68
+ taken: já existe neste país
69
+ abbr:
70
+ taken: já existe neste país
71
+ slug:
72
+ taken: já existe neste país
73
+ city:
74
+ attributes:
75
+ name:
76
+ taken: já existe nesta região
77
+ slug:
78
+ taken: já está em uso por outra cidade
79
+ hood:
80
+ attributes:
81
+ name:
82
+ taken: já existe nesta cidade
83
+ slug:
84
+ taken: já está em uso por outro bairro
59
85
  geopolitical:
60
86
  title: Geopolítico
61
87
  actions:
data/geopolitical.gemspec CHANGED
@@ -21,5 +21,5 @@ Gem::Specification.new do |s|
21
21
  s.required_ruby_version = '>= 3.2.0'
22
22
 
23
23
  s.add_dependency 'mongoid', '>= 8.0.0'
24
- s.add_dependency 'mongoid-geospatial', '>= 7.0.0'
24
+ s.add_dependency 'mongoid-geospatial', '>= 7.2.0'
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geopolitical
4
- VERSION = '4.1.1'
4
+ VERSION = '4.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geopolitical
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcos Piccinini
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 7.0.0
32
+ version: 7.2.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 7.0.0
39
+ version: 7.2.0
40
40
  description: 'The whole planet as four Mongoid models: hierarchy, i18n names, unicode-safe
41
41
  slugs, geo queries, phone/postal fallbacks and an admin engine.'
42
42
  email:
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  requirements: []
126
- rubygems_version: 3.6.7
126
+ rubygems_version: 4.0.20
127
127
  specification_version: 4
128
128
  summary: Nation, Region, City and Hood models for Mongoid.
129
129
  test_files: []