Siftly::Rails
siftly-rails wires Siftly.check into Active Model and Active Record validations. Use it when you want attributes checked automatically during validation and want the results attached to the model instance.
Installation
gem "siftly-rails"require "siftly/rails"Requiring siftly/rails auto-includes Siftly::Rails::Model into all Active Record models via ActiveSupport.on_load(:active_record). You still need to require whichever plugin gems provide the filters you want — siftly-rails only adds the model integration layer.
For plain Active Model objects that don't inherit from ActiveRecord::Base, include the concern manually with include Siftly::Rails::Model.
Quick Start
First configure the normal Siftly pipeline:
Siftly.configure do |config|
config.aggregator = :score
config.threshold = 1.0
config.use :keyword_pack
config.filter :keyword_pack do |filter|
filter.keywords = ["spam", "buy now"]
filter.weight = 1.0
end
endThen declare the attributes to check:
class ContactRequest < ApplicationRecord
siftly_check :email, :message, message: "contains spam"
endAt validation time, each declared attribute is checked with:
-
value:the attribute value -
attribute:the attribute name -
record:the model instance -
context:resolved from the check options -
filter_overrides:resolved from the check options
Example:
request = ContactRequest.new(email: "spam@example.com", message: "buy now")
request.valid? # => false
request.siftly_spam? # => true
request.siftly_spam_attributes # => [:email, :message]
request.siftly_results.keys # => [:email, :message]
request.siftly_spam_result_for(:message) # => #<Siftly::Result ...>What siftly_check Does
Each siftly_check call:
- registers one validation callback for the declared attributes
- runs
Siftly.checkonce per attribute - stores the latest result per attribute in
@siftly_results - adds an Active Model error when a result is spam
The spam-tracking state is reset in before_validation, so old results do not leak across revalidation.
Supported Options
Siftly options passed through to Siftly.check:
filtersfilter_overridesaggregatorthresholdfailure_modeinstrumentercontext
Model-side validation behavior:
allow_nilallow_blankmessagestrict
Active Model callback options:
ifunlesson
Option Resolution
Every non-callback option can be a literal value or a callable. This is broader than just context and filter_overrides.
Callables are executed via instance_exec on the model instance, so self inside the block is the record. Arity determines which explicit arguments are passed:
- arity
0: no arguments (access the record throughself) - arity
1:attribute - arity
2+:attributeandrecord
Example:
class Lead < ApplicationRecord
siftly_check :message,
filters: -> { [:keyword_pack] },
filter_overrides: ->(attribute) { { keyword_pack: { keywords: [trigger_phrase], weight: 1.2 } } },
context: ->(attribute, record) { { source: "#{attribute}:#{record.source_name}" } },
allow_blank: true
endSkip Behavior
A check is skipped entirely when:
-
allow_nilis truthy and the value isnil -
allow_blankis truthy and the value is blank
Skipped checks:
- do not call
Siftly.check - do not store a result for that attribute
- do not add errors
Exposed Instance Helpers
Instances get:
siftly_spam?siftly_resultssiftly_result_for(attribute)siftly_spam_attributessiftly_spam_resultssiftly_spam_result_for(attribute)
Practical Advice
- Scope
filters:per model when different models need different spam rules. Relying only on global configuration is how you end up checking everything with everything. - Remember that
siftly-railsdoes not decide what counts as spam. It just runs the configured pipeline during validation. - If a filter is flaky or network-backed, set
failure_modedeliberately. Pretending the default is fine without thinking about it is lazy.