safer_initialize 0.2.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: 31eb1c05500e2d20207203e3d4d65a29507920d6f0da6b0cadaef0d87df81b12
4
- data.tar.gz: 9d8f0360e41cb13a56314c79c3bb64e46ba892ffcc40351d1ec241ab96f53b0e
3
+ metadata.gz: bd32a618d97effbb7546a3a02377b877291583dac0aa12c2d7269bc73f4c9f65
4
+ data.tar.gz: bd01330e3335b3a33d5414b43330ecaf438f160e9e97f7d72b76a6079130f9b3
5
5
  SHA512:
6
- metadata.gz: f1cb3bbc1654debbf28f5e336bd46bc7f03ad9ed3e65d1e651f41e3a83bff1591a3e5e4418f41027cc557e78796e299c40537a590cbb5ebc34b762e14df0a8c0
7
- data.tar.gz: 5dd462b38e4b9058de7eccf5853aa788133d2374282f3af2297b02cdacef1b20e22a77600f7546e0cb6ca4c9785f17d92e0ffea4c171dbd46710c31e30a38482
6
+ metadata.gz: 6bbe298665a9188db3c440a6f5fa37a9e15499c51479179cafd29759f42e024f4036200df689b721d25940d57dc9771e51bcd326be9de9e3d55bbb55811f6252
7
+ data.tar.gz: 5c1b45239a851a50bb4231721e624940693d2240797c333dbfb7d51f7f36556d9d27509642983021b4683c054717ead6207d1abc41d1c8e8171b4f29f6a0fd65
@@ -0,0 +1,108 @@
1
+ ---
2
+ description: Maintain the Ruby and Rails version matrix tested in CI for a Ruby gem. Use this when you need to remove EOL versions, add the latest supported versions, and update gemfiles and gemspec accordingly. Always use this for requests like 'remove unsupported Ruby/Rails versions', 'add the latest Rails', or 'update the CI matrix'.
3
+ license: MIT
4
+ metadata:
5
+ github-path: skills/ruby/ruby-rails-ci-matrix
6
+ github-ref: refs/heads/main
7
+ github-repo: https://github.com/aki77/skills
8
+ github-tree-sha: 59898557bcf3dd4e734ae3f25c8ab025a43341be
9
+ name: ruby-rails-ci-matrix
10
+ ---
11
+ # ruby-rails-ci-matrix
12
+
13
+ Maintain the Ruby and Rails versions tested in CI for a Ruby gem that uses GitHub Actions matrix strategy with `gemfiles/railsXX.gemfile` and a gemspec. This skill keeps the test matrix aligned with the current support status.
14
+
15
+ Remove EOL (End of Life) versions and add currently supported and latest stable versions. The gemfile, gemspec, and workflow YAML must be updated **in sync** — updating only one will break CI.
16
+
17
+ ## 1. Assess the Current State
18
+
19
+ Before making any changes, read all three types of files involved.
20
+
21
+ | File | What to check |
22
+ | --- | --- |
23
+ | `.github/workflows/*.yml` | `strategy.matrix.include` entries (`{ ruby:, gemfile: }`), `ruby/setup-ruby` version |
24
+ | `gemfiles/*.gemfile` | Per-Rails-version gemfiles — `source` URL and `gem 'rails', '~> X.Y.0'` |
25
+ | `*.gemspec` | `required_ruby_version` and the lower bound of `add_dependency "rails", ...` |
26
+
27
+ Note that the `gemfile:` values in the matrix correspond 1:1 with `gemfiles/<value>.gemfile`.
28
+
29
+ ## 2. Research EOL Status
30
+
31
+ **Use today's date as the reference** and fetch the latest support information — do not rely on memory.
32
+
33
+ - Ruby: <https://endoflife.date/ruby>
34
+ - Rails: <https://endoflife.date/rails>
35
+
36
+ If endoflife.date is unreachable, use WebSearch as a fallback. Confirm the following three things:
37
+
38
+ 1. EOL date for each version (including security support end date) — has it passed as of today?
39
+ 2. Full list of currently supported versions
40
+ 3. Latest stable releases (check whether a new major like Ruby 4.0 or Rails 8.1 has been released)
41
+
42
+ > Ruby releases a new version every December with ~3 years 3 months of support. Rails 7.2+ receives 1 year of standard support and 2 years of security support.
43
+
44
+ ## 3. Decide What to Add and Remove
45
+
46
+ - **Remove**: Ruby / Rails versions that are EOL (security support also ended) as of today.
47
+ - **Add**: Supported versions not yet in the matrix, plus the latest stable releases.
48
+ - **Keep**: Versions still under security support. For versions expiring within a few months, ask the user whether to keep them.
49
+ - Build the matrix as **each Rails version × the compatible Ruby versions**. Pay attention to compatibility — older Rails versions may not support the newest Ruby.
50
+
51
+ ## 4. Update Files
52
+
53
+ Keep all three files consistent. **If you remove a gemfile, remove the matrix entry too** (and vice versa).
54
+
55
+ ### Workflow YAML Matrix
56
+
57
+ Update the `include:` entries. Use short-form Ruby version strings (`"4.0"`) so `ruby/setup-ruby` automatically picks the latest patch.
58
+
59
+ ```yaml
60
+ strategy:
61
+ matrix:
62
+ include:
63
+ - { ruby: "3.3", gemfile: "rails72" }
64
+ - { ruby: "3.4", gemfile: "rails72" }
65
+ - { ruby: "4.0", gemfile: "rails72" }
66
+ - { ruby: "3.3", gemfile: "rails80" }
67
+ - { ruby: "3.4", gemfile: "rails80" }
68
+ - { ruby: "4.0", gemfile: "rails80" }
69
+ - { ruby: "3.3", gemfile: "rails81" }
70
+ - { ruby: "3.4", gemfile: "rails81" }
71
+ - { ruby: "4.0", gemfile: "rails81" }
72
+ ```
73
+
74
+ ### Create / Delete Gemfiles
75
+
76
+ For a new Rails version, copy an existing gemfile and change only the `~> X.Y.0` constraint.
77
+
78
+ ```ruby
79
+ source "https://rubygems.org"
80
+
81
+ gem 'rails', '~> 8.1.0'
82
+ gem 'sqlite3'
83
+
84
+ gemspec path: '../'
85
+ ```
86
+
87
+ Delete the gemfile for any Rails version being dropped.
88
+
89
+ ### Update gemspec Lower Bounds
90
+
91
+ Update two lines to match the minimum supported versions.
92
+
93
+ ```ruby
94
+ spec.required_ruby_version = '>= 3.3.0' # minimum supported Ruby
95
+ spec.add_dependency "rails", ">= 7.2.0" # minimum supported Rails
96
+ ```
97
+
98
+ ## Notes
99
+
100
+ - **Always use `https://rubygems.org`** as the `source` — fix any `http://` occurrences.
101
+ - **`ruby/setup-ruby@v1` and new majors**: For brand-new majors like Ruby 4.0, verify that setup-ruby already supports it (it usually does very quickly).
102
+ - **Raising `required_ruby_version` is a breaking change**. Users on the old Ruby version will no longer be able to install the gem, so a minor or major gem version bump is required at release time. Handle this in a separate PR / commit, not as part of this matrix update.
103
+
104
+ ## Pitfalls
105
+
106
+ - **Forgetting to update the gemspec lower bounds** means the old Ruby/Rails version you removed from CI can still be `gem install`-ed, creating a mismatch between stated and actual support.
107
+ - **Updating only the gemfile or only the matrix** breaks CI — either a missing gemfile is referenced, or an unused gemfile is left behind. Always update both together.
108
+ - **Mixing unrelated changes** (e.g., fixing `http://` → `https://` sources) into the same commit as version removals obscures intent. Commit each logical unit separately.
@@ -0,0 +1,38 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ rspec:
10
+ runs-on: ubuntu-latest
11
+ env:
12
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ include:
17
+ - { ruby: "3.3", gemfile: "gemfiles/7.2.gemfile" }
18
+ - { ruby: "3.4", gemfile: "gemfiles/7.2.gemfile" }
19
+ - { ruby: "4.0", gemfile: "gemfiles/7.2.gemfile" }
20
+ - { ruby: "3.3", gemfile: "gemfiles/8.0.gemfile" }
21
+ - { ruby: "3.4", gemfile: "gemfiles/8.0.gemfile" }
22
+ - { ruby: "4.0", gemfile: "gemfiles/8.0.gemfile" }
23
+ - { ruby: "3.3", gemfile: "gemfiles/8.1.gemfile" }
24
+ - { ruby: "3.4", gemfile: "gemfiles/8.1.gemfile" }
25
+ - { ruby: "4.0", gemfile: "gemfiles/8.1.gemfile" }
26
+ steps:
27
+ - name: Install packages
28
+ run: |
29
+ sudo apt update -y
30
+ sudo apt install -y libsqlite3-dev
31
+ - uses: actions/checkout@v7
32
+ - name: Set up Ruby
33
+ uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: ${{ matrix.ruby }}
36
+ bundler-cache: true
37
+ - name: Run rspec
38
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  Gemfile.lock
13
+ test.sqlite3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## Unreleased
2
+
3
+ ## 0.4.0
4
+
5
+ - Add `SaferInitialize.defer` to avoid N+1 checks (#6)
6
+ - Drop support for Ruby < 3.3 and Rails < 7.2; test against Ruby 3.3/3.4/4.0 and Rails 7.2/8.0/8.1
7
+
8
+ ## 0.3.0
9
+
10
+ - Support message option with Proc (#3)
11
+
1
12
  ## 0.2.0
2
13
 
3
14
  - Add `error_handle` config
data/Gemfile CHANGED
@@ -3,5 +3,6 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in safer_initialize.gemspec
4
4
  gemspec
5
5
 
6
- gem "rake", "~> 12.0"
7
- gem "rspec", "~> 3.0"
6
+ gem "rake"
7
+ gem "rspec"
8
+ gem "sqlite3"
data/README.md CHANGED
@@ -26,7 +26,7 @@ In `config/initializers/safer_initialize.rb`:
26
26
  SaferInitialize::Globals.attribute :tenant
27
27
 
28
28
  SaferInitialize.configure do |config|
29
- config.error_handle = -> (e) { Rails.env.production? ? Bugsnag.notify(e) : raise e } # default: -> (e) { raise e }
29
+ config.error_handle = -> (e) { Rails.env.production? ? Bugsnag.notify(e) : raise(e) } # default: -> (e) { raise e }
30
30
  end
31
31
  ```
32
32
 
@@ -78,6 +78,41 @@ In `app/views/projects/index.html.haml`:
78
78
  = project.name
79
79
  ```
80
80
 
81
+ ## Deferred evaluation (avoiding N+1)
82
+
83
+ `safer_initialize` runs inside `after_initialize`, which fires while each record
84
+ is being built — *before* Rails resolves `includes`/`preload`. If your check
85
+ touches an association, it triggers a query per record (N+1), even when the
86
+ query eager-loads that association.
87
+
88
+ Wrap the load in `SaferInitialize.defer` to queue the checks and evaluate them
89
+ after the block finishes, once preloading is done:
90
+
91
+ ```ruby
92
+ SaferInitialize.defer do
93
+ # Loading through `user.projects` sets the inverse for `project.user`, but not for
94
+ # `project.tenant` — so the tenant check's `project.tenant` needs a query per row.
95
+ # `includes(:tenant)` eager-loads it, but the check runs in `after_initialize` —
96
+ # before the preload resolves — so it still N+1s without `defer`.
97
+ projects = current_user.projects.includes(:tenant).to_a
98
+ # checks are queued here, not evaluated yet
99
+ render_something(projects)
100
+ end
101
+ # checks run at the block end — the preload is done, so `project.tenant` is cached (no N+1)
102
+ ```
103
+
104
+ (`current_tenant.projects` would not N+1 here: loading through that association sets the
105
+ inverse, so `project.tenant` is already resolved and `includes(:tenant)` is unnecessary.)
106
+
107
+ Notes:
108
+
109
+ - Outside a `defer` block the behavior is unchanged (immediate evaluation).
110
+ - Globals (e.g. `tenant`) are expected to stay constant inside the block; set
111
+ them before entering `defer`, not within it. `with_safe` still skips checks
112
+ as usual.
113
+ - If the block raises, the queue is discarded without running the checks.
114
+ - `defer` cannot be nested.
115
+
81
116
  ## License
82
117
 
83
118
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~> 7.2.0'
4
+ gem "rake", "~> 13.0"
5
+ gem "rspec", "~> 3.0"
6
+ gem "sqlite3", "~> 1.4"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~> 8.0.0'
4
+ gem "rake", "~> 13.0"
5
+ gem "rspec", "~> 3.0"
6
+ gem "sqlite3", "~> 2.1"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~> 8.1.0'
4
+ gem "rake", "~> 13.0"
5
+ gem "rspec", "~> 3.0"
6
+ gem "sqlite3", "~> 2.1"
7
+
8
+ gemspec :path => "../"
@@ -6,15 +6,25 @@ module SaferInitialize
6
6
  class_methods do
7
7
  def safer_initialize(filter = nil, message: 'initialize error', &block)
8
8
  after_initialize do |object|
9
- unless SaferInitialize::Globals.safe?
10
- result = filter ? object.send(filter) : object.instance_exec(object, &block)
11
- unless result
12
- SaferInitialize.configuration.error_handle.call(SaferInitialize::Error.new(message))
13
- end
9
+ next if SaferInitialize::Globals.safe?
10
+
11
+ check = -> { SaferInitialize::ActiveRecord::Extensions.run_check(object, filter, message, block) }
12
+ if SaferInitialize::Globals.deferring?
13
+ SaferInitialize::Globals.__deferred_checks << check
14
+ else
15
+ check.call
14
16
  end
15
17
  end
16
18
  end
17
19
  end
20
+
21
+ def self.run_check(object, filter, message, block)
22
+ result = filter ? object.send(filter) : object.instance_exec(object, &block)
23
+ return if result
24
+
25
+ message_text = message.respond_to?(:call) ? message.call(object) : message
26
+ SaferInitialize.configuration.error_handle.call(SaferInitialize::Error.new(message_text))
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -2,10 +2,14 @@ require 'active_support'
2
2
 
3
3
  module SaferInitialize
4
4
  class Globals < ActiveSupport::CurrentAttributes
5
- attribute :__safe
5
+ attribute :__safe, :__deferring, :__deferred_checks
6
6
 
7
7
  def safe?
8
8
  !!__safe
9
9
  end
10
+
11
+ def deferring?
12
+ !!__deferring
13
+ end
10
14
  end
11
15
  end
@@ -1,3 +1,3 @@
1
1
  module SaferInitialize
2
- VERSION = '0.2.0'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -21,4 +21,27 @@ module SaferInitialize
21
21
  def with_safe(&block)
22
22
  Globals.set(__safe: true, &block)
23
23
  end
24
+
25
+ # Defer safer_initialize checks queued inside the block until the block
26
+ # finishes, so associations preloaded by the enclosed queries are already
27
+ # resolved by the time the checks run (avoids N+1). Globals must not be
28
+ # mutated inside the block. On an exception the queue is discarded without
29
+ # flushing. Cannot be nested.
30
+ def defer
31
+ raise Error, 'SaferInitialize.defer cannot be nested' if Globals.deferring?
32
+
33
+ Globals.__deferring = true
34
+ Globals.__deferred_checks = []
35
+
36
+ result = yield
37
+
38
+ checks = Globals.__deferred_checks
39
+ Globals.__deferring = nil
40
+ Globals.__deferred_checks = nil
41
+ checks.each(&:call)
42
+ result
43
+ ensure
44
+ Globals.__deferring = nil
45
+ Globals.__deferred_checks = nil
46
+ end
24
47
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = %q{Make ActiveRecord initialization less dangerous.}
11
11
  spec.homepage = "https://github.com/aki77/safer_initialize"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = spec.homepage
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ["lib"]
26
26
 
27
- spec.add_dependency 'rails', '>= 6.0.0'
27
+ spec.add_dependency 'rails', '>= 7.2.0'
28
28
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safer_initialize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aki77
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2021-03-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 6.0.0
18
+ version: 7.2.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 6.0.0
25
+ version: 7.2.0
27
26
  description: Make ActiveRecord initialization less dangerous.
28
27
  email:
29
28
  - aki77@users.noreply.github.com
@@ -31,6 +30,8 @@ executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
33
32
  files:
33
+ - ".claude/skills/ruby-rails-ci-matrix/SKILL.md"
34
+ - ".github/workflows/rspec.yml"
34
35
  - ".gitignore"
35
36
  - ".rspec"
36
37
  - ".travis.yml"
@@ -42,6 +43,9 @@ files:
42
43
  - Rakefile
43
44
  - bin/console
44
45
  - bin/setup
46
+ - gemfiles/7.2.gemfile
47
+ - gemfiles/8.0.gemfile
48
+ - gemfiles/8.1.gemfile
45
49
  - lib/safer_initialize.rb
46
50
  - lib/safer_initialize/active_record/extensions.rb
47
51
  - lib/safer_initialize/configuration.rb
@@ -55,7 +59,6 @@ licenses:
55
59
  metadata:
56
60
  homepage_uri: https://github.com/aki77/safer_initialize
57
61
  source_code_uri: https://github.com/aki77/safer_initialize
58
- post_install_message:
59
62
  rdoc_options: []
60
63
  require_paths:
61
64
  - lib
@@ -63,15 +66,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
66
  requirements:
64
67
  - - ">="
65
68
  - !ruby/object:Gem::Version
66
- version: 2.5.0
69
+ version: 3.3.0
67
70
  required_rubygems_version: !ruby/object:Gem::Requirement
68
71
  requirements:
69
72
  - - ">="
70
73
  - !ruby/object:Gem::Version
71
74
  version: '0'
72
75
  requirements: []
73
- rubygems_version: 3.1.2
74
- signing_key:
76
+ rubygems_version: 4.0.10
75
77
  specification_version: 4
76
78
  summary: Make ActiveRecord initialization less dangerous.
77
79
  test_files: []