Add ClientStub, setup/client_for API, and Railtie#10
Conversation
Adds three new components for Rails integration: - Rspamd::ClientStub: null-object client that returns ham for check and no-ops for training, used when rspamd is disabled (dev/test) - Rspamd::Rails: thread-safe client management with config loading via setup(config) and client_for(:outbound) - Rspamd::Railtie: auto-loads config/rspamd.yml on Rails boot Also adds activesupport as a runtime dependency for deep_symbolize_keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Rails integration helpers for rspamd-ruby so Rails apps can auto-configure and obtain per-purpose clients, with a null-object stub when disabled.
Changes:
- Introduces
Rspamd::ClientStub(null client) andRspamd::Rails(client/config management) plus aRspamd::Railtiefor auto-setup. - Adds
activesupportruntime dependency to supportdeep_symbolize_keys. - Adds test coverage for the new stub and Rails helper.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
lib/rspamd/client_stub.rb |
Adds a null-object client for disabled/dev/test usage. |
lib/rspamd/rails.rb |
Adds Rails-facing setup + client lookup/caching. |
lib/rspamd/railtie.rb |
Auto-initializes configuration from config/rspamd.yml in Rails. |
lib/rspamd-ruby.rb |
Wires new files into the gem entrypoint. |
rspamd-ruby.gemspec |
Adds activesupport as a runtime dependency. |
test/client_stub_test.rb |
Tests expected stub behaviors. |
test/rails_test.rb |
Tests Rails helper setup, caching, reset, and stub fallback. |
Gemfile.lock |
Updates lockfile to include new dependency and other dependency changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lib/rspamd/rails.rb
Outdated
| @config = config.deep_symbolize_keys | ||
| @clients = {} | ||
| end | ||
|
|
||
| def client_for(name) | ||
| return ClientStub.new unless enabled? | ||
|
|
||
| @clients[name] ||= build_client(name) | ||
| end | ||
|
|
||
| def reset! | ||
| @config = nil | ||
| @clients = {} | ||
| end | ||
|
|
||
| private | ||
| def enabled? | ||
| @config&.dig(:enabled) |
There was a problem hiding this comment.
client_for caches clients with @clients[name] ||= ... and setup/reset! mutate @clients/@config without any synchronization. This is not thread-safe under multi-threaded Rails servers (can build multiple clients, or race with reset and raise due to nil config). Consider guarding config/client access with a Mutex (or using a thread-safe map) to ensure only one client is built per name and reads/writes are consistent.
| @config = config.deep_symbolize_keys | |
| @clients = {} | |
| end | |
| def client_for(name) | |
| return ClientStub.new unless enabled? | |
| @clients[name] ||= build_client(name) | |
| end | |
| def reset! | |
| @config = nil | |
| @clients = {} | |
| end | |
| private | |
| def enabled? | |
| @config&.dig(:enabled) | |
| mutex.synchronize do | |
| @config = config.deep_symbolize_keys | |
| @clients = {} | |
| end | |
| end | |
| def client_for(name) | |
| mutex.synchronize do | |
| return ClientStub.new unless @config&.dig(:enabled) | |
| @clients ||= {} | |
| @clients[name] ||= build_client(name) | |
| end | |
| end | |
| def reset! | |
| mutex.synchronize do | |
| @config = nil | |
| @clients = {} | |
| end | |
| end | |
| private | |
| def mutex | |
| @mutex ||= Mutex.new | |
| end | |
| def enabled? | |
| mutex.synchronize do | |
| @config&.dig(:enabled) | |
| end |
| HAM_RESULT = Check::Result.new( | ||
| "score" => 0.0, | ||
| "required_score" => 15.0, | ||
| "action" => "no action", | ||
| "is_skipped" => false, | ||
| "symbols" => {}, | ||
| "urls" => [], | ||
| "emails" => [] | ||
| ).freeze |
There was a problem hiding this comment.
HAM_RESULT is a single shared Check::Result instance returned by check. While the result object is frozen, the nested data hash/arrays (and e.g. symbols) remain mutable, so any consumer mutation will leak across calls/threads. Consider returning a fresh Check::Result per call, or deep-freezing/duplicating the nested data before exposing it.
test/rails_test.rb
Outdated
| def test_accepts_string_keys | ||
| Rspamd::Rails.setup("enabled" => true, "outbound" => { "host" => "localhost", "port" => 11334 }) | ||
|
|
||
| client = Rspamd::Rails.client_for(:outbound) | ||
| assert_instance_of Rspamd::Client, client | ||
| end |
There was a problem hiding this comment.
test_accepts_string_keys appears redundant with test_returns_client_when_enabled (both already pass string keys to setup). If the intent is to verify symbol-vs-string handling, consider adjusting one test to pass symbol keys (or remove this test if it's not adding distinct coverage).
- Add explicit require for check/result in client_stub.rb so the file is standalone-loadable - Rename test_accepts_string_keys to test_accepts_symbol_keys and actually pass symbol keys to provide distinct coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When disabled, client_for now caches the ClientStub per name instead of returning a new instance each call. This ensures the same object is returned on repeated calls, which is needed for test expectations (e.g. Rspamd.outbound_client.expects(:check)). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lib/rspamd-ruby.rb
Outdated
| require "rspamd/client" | ||
| require "rspamd/client_stub" | ||
| require "rspamd/errors" | ||
| require "rspamd/rails" |
There was a problem hiding this comment.
rspamd/rails is unconditionally required here, but it in turn does require "active_support/core_ext/hash/keys". This means every user of this gem (including non-Rails projects) is forced to install and load activesupport as a runtime dependency.
The Railtie is already conditionally loaded (if defined?(::Rails::Railtie)), but rspamd/rails should receive the same treatment — either guard it similarly (e.g., require "rspamd/rails" if defined?(::ActiveSupport)) or move the require into the Railtie so it's only loaded in Rails apps.
| require "rspamd/rails" | |
| require "rspamd/rails" if defined?(::ActiveSupport) |
|
|
||
| s.required_ruby_version = ">= 2.7.8" | ||
|
|
||
| s.add_dependency "activesupport", ">= 6.0" |
There was a problem hiding this comment.
Adding activesupport as an unconditional runtime dependency is a heavy requirement for what is currently used only for deep_symbolize_keys in Rspamd::Rails. This forces all consumers of this gem (including non-Rails projects) to pull in activesupport and its transitive dependencies.
Consider either:
- Making
activesupportan optional dependency and only requiring it in the Rails-specific code path, or - Implementing the
deep_symbolize_keyslogic inline (it's a small recursive method) to avoid the dependency entirely for the core gem.
| s.add_dependency "activesupport", ">= 6.0" | |
| s.add_development_dependency "activesupport", ">= 6.0" |
The Rails module name was misleading since this code works outside Rails. Moving it to the top-level Rspamd module simplifies the API: Rspamd.setup(config) Rspamd.client_for(:outbound) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Rspamd::ClientStub— Null-object client that returns ham forcheckand no-ops for training methods. Used when rspamd is disabled (dev/test environments).Rspamd.setup(config)/Rspamd.client_for(:name)— Thread-safe client management with config loading. Returns cachedRspamd::Clientinstances (orClientStubwhen disabled).Rspamd::Railtie— Auto-callsRspamd.setupafter Rails init ifconfig/rspamd.ymlexists.Usage
Test plan
🤖 Generated with Claude Code