Skip to content

Add ClientStub, setup/client_for API, and Railtie#10

Open
rosa wants to merge 4 commits intomasterfrom
enhance-with-rails-features
Open

Add ClientStub, setup/client_for API, and Railtie#10
rosa wants to merge 4 commits intomasterfrom
enhance-with-rails-features

Conversation

@rosa
Copy link
Member

@rosa rosa commented Mar 4, 2026

Summary

  • Rspamd::ClientStub — Null-object client that returns ham for check and 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 cached Rspamd::Client instances (or ClientStub when disabled).
  • Rspamd::Railtie — Auto-calls Rspamd.setup after Rails init if config/rspamd.yml exists.

Usage

# config/rspamd.yml
production:
  enabled: true
  inbound:
    host: rspamd-in.example.com
    port: 11334
    password: secret
  outbound:
    host: rspamd-out.example.com
    port: 11334
    password: secret

# Railtie auto-configures, then:
Rspamd.client_for(:inbound).check(message)
Rspamd.client_for(:outbound).spam!(message)

Test plan

  • ClientStub tests (7 tests)
  • Setup/client_for tests (8 tests)
  • All existing tests pass (31 runs, 57 assertions)
  • Integrated and tested in HEY (haystack) — 44 rspamd-related tests pass

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 4, 2026 20:50
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and Rspamd::Rails (client/config management) plus a Rspamd::Railtie for auto-setup.
  • Adds activesupport runtime dependency to support deep_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.

Comment on lines +9 to +26
@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)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
@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

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +11
HAM_RESULT = Check::Result.new(
"score" => 0.0,
"required_score" => 15.0,
"action" => "no action",
"is_skipped" => false,
"symbols" => {},
"urls" => [],
"emails" => []
).freeze
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
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
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
rosa and others added 2 commits March 4, 2026 21:13
- 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>
Copilot AI review requested due to automatic review settings March 4, 2026 21:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

require "rspamd/client"
require "rspamd/client_stub"
require "rspamd/errors"
require "rspamd/rails"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
require "rspamd/rails"
require "rspamd/rails" if defined?(::ActiveSupport)

Copilot uses AI. Check for mistakes.

s.required_ruby_version = ">= 2.7.8"

s.add_dependency "activesupport", ">= 6.0"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Making activesupport an optional dependency and only requiring it in the Rails-specific code path, or
  2. Implementing the deep_symbolize_keys logic inline (it's a small recursive method) to avoid the dependency entirely for the core gem.
Suggested change
s.add_dependency "activesupport", ">= 6.0"
s.add_development_dependency "activesupport", ">= 6.0"

Copilot uses AI. Check for mistakes.
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>
@rosa rosa changed the title Add ClientStub, Rails module, and Railtie Add ClientStub, setup/client_for API, and Railtie Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants