Skip to content

feat(scopable): delegate filters to declared model scopes - #13

Merged
jplot merged 5 commits into
mainfrom
feat/scopable
Jul 3, 2026
Merged

feat(scopable): delegate filters to declared model scopes#13
jplot merged 5 commits into
mainfrom
feat/scopable

Conversation

@jplot

@jplot jplot commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Next item from the gem-landscape backlog: expose existing model scopes as filters, in two explicit forms — no arity detection, no respond_to?-derived whitelist (the anti-patterns the gem review flagged in toschas/active_filterable/filterameter).

Changes

  • scopable :cheaper_than: hands the normalized filters[cheaper_than] value to the scope (.cheaper_than(250)); blank or non-scalar values narrow nothing (shares ValueNormalization with equatable, extracted here as a sibling of AttributeNormalization)
  • togglable :priced: a bare key (?filters[priced], filters[priced]=) or a strict true value (true/'true'/1/'1') applies the no-argument scope; explicit false values ('false', '0' — what Rails' check_box hidden field submits) and arbitrary strings narrow nothing. Stricter than ActiveModel::Type::Boolean, where any unrecognized string casts to true — specs lock both the bare-key toggle and this divergence
  • never-raise holds end to end: a declared name whose scope does not exist, or a scope not returning a relation, is a silent no-op; the declarations validator now checks scope-backed kinds and reports scopable: 'x' maps to unknown scope 'y'
  • aliasing (scopable max_price: :cheaper_than) and subclass inheritance as in the other DSLs; both filters report through applied_filters; Railtie includes the two concerns; README documents everything

How to Test

  1. bundle exec rake
  2. MovementDetail.filterable(filters: { cheaper_than: 250 }).cheaper_than(250) ; filters: { priced: '' } (clé nue) → .priced ; filters: { priced: '0' } → relation intacte

jplot added 3 commits July 3, 2026 13:23
- Filterable::ValueNormalization mirrors AttributeNormalization: one
  shared home for coercing raw params values, so upcoming filters can
  reuse it instead of reaching into Equatable
- `scopable :cheaper_than` hands the normalized `filters[<name>]`
  value to the scope; `togglable :priced` applies a no-argument scope
  on a strict true value (true/1/'1'/'true') — checkbox semantics,
  deliberately stricter than ActiveModel::Type::Boolean where any
  unrecognized string casts to true
- the scope name is fixed at declaration time, never derived from the
  params or from respond_to?; a missing scope or a scope not returning
  a relation narrows nothing, per the never-raise contract
- aliasing and subclass inheritance as in the other DSLs; both filters
  report through applied_filters; Railtie includes the two concerns
- declarations validator gains scope-backed kinds: a declared name
  whose scope does not exist is reported as an error
Copilot AI review requested due to automatic review settings July 3, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

📊 Code Quality Report

Tool Base PR Δ
Reek 26 40 smells 🔴 +14
Flog total 448.7 577.7 🔴 +129.0
Flay total 439 521 🔴 +82

🔍 16 new reek smells introduced — review below.

🔍 16 new reek smells introduced by this PR
Smell File Context Lines Message
DataClump lib/filterable/scopable.rb Filterable::Scopable 17, 30, 44 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/togglable.rb Filterable::Togglable 24, 37, 51 takes parameters ['params', 'scope'] to 3 methods
DuplicateMethodCall lib/filterable/scopable.rb Filterable::Scopable#accepted 50, 52 calls 'declared[name]' 2 times
DuplicateMethodCall lib/filterable/togglable.rb Filterable::Togglable#accepted 56, 58 calls 'declared[name]' 2 times
DuplicateMethodCall lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 73, 75 calls 'TRUE_VALUES.include?(value)' 2 times
ManualDispatch lib/filterable/concerns/scopable.rb Filterable::Concerns::Scopable#scopable_scope_names 24 manually dispatches method call
ManualDispatch lib/filterable/concerns/togglable.rb Filterable::Concerns::Togglable#togglable_scope_names 25 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#column_errors 59 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#scope_errors 76, 79 manually dispatches method call
ManualDispatch lib/filterable/scopable.rb Filterable::Scopable#accepted 47, 50 manually dispatches method call
ManualDispatch lib/filterable/togglable.rb Filterable::Togglable#accepted 54, 56 manually dispatches method call
NilCheck lib/filterable/scopable.rb Filterable::Scopable#accepted 50 performs a nil-check
NilCheck lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 70 performs a nil-check
TooManyStatements lib/filterable/scopable.rb Filterable::Scopable#accepted 44 has approx 7 statements
TooManyStatements lib/filterable/togglable.rb Filterable::Togglable#accepted 51 has approx 6 statements
TooManyStatements lib/filterable/value_normalization.rb Filterable::ValueNormalization#normalize 15 has approx 6 statements
✅ 2 reek smells resolved by this PR

The exact list isn't shown — these smells are gone, that's what matters.

🔍 All 40 reek smells on this PR
Smell File Context Lines Message
DataClump lib/filterable/equatable.rb Filterable::Equatable 18, 31, 43, 57 takes parameters ['params', 'scope'] to 4 methods
DataClump lib/filterable/scopable.rb Filterable::Scopable 17, 30, 44 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/sortable.rb Filterable::Sortable 19, 33, 44 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/togglable.rb Filterable::Togglable 24, 37, 51 takes parameters ['params', 'scope'] to 3 methods
DuplicateMethodCall lib/filterable/concern.rb Filterable::Concern#applied_filters 68, 71 calls 'params[:filters]' 2 times
DuplicateMethodCall lib/filterable/concern.rb Filterable::Concern#filterable 49, 51 calls 'params[:filters]' 2 times
DuplicateMethodCall lib/filterable/scopable.rb Filterable::Scopable#accepted 50, 52 calls 'declared[name]' 2 times
DuplicateMethodCall lib/filterable/sortable.rb Filterable::Sortable#terms 45, 47 calls 'params[:sort]' 2 times
DuplicateMethodCall lib/filterable/togglable.rb Filterable::Togglable#accepted 56, 58 calls 'declared[name]' 2 times
DuplicateMethodCall lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 73, 75 calls 'TRUE_VALUES.include?(value)' 2 times
ManualDispatch lib/filterable/concern.rb Filterable::Concern#add_filter 33 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#applied_filters 70 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#filters 21 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#indifferent? 92 manually dispatches method call
ManualDispatch lib/filterable/concerns/datable.rb Filterable::Concerns::Datable#datable_attribute_names 27 manually dispatches method call
ManualDispatch lib/filterable/concerns/equatable.rb Filterable::Concerns::Equatable#equatable_attribute_names 25 manually dispatches method call
ManualDispatch lib/filterable/concerns/rangeable.rb Filterable::Concerns::Rangeable#rangeable_attribute_names 26 manually dispatches method call
ManualDispatch lib/filterable/concerns/scopable.rb Filterable::Concerns::Scopable#scopable_scope_names 24 manually dispatches method call
ManualDispatch lib/filterable/concerns/sortable.rb Filterable::Concerns::Sortable#sortable_attribute_names 24 manually dispatches method call
ManualDispatch lib/filterable/concerns/togglable.rb Filterable::Concerns::Togglable#togglable_scope_names 25 manually dispatches method call
ManualDispatch lib/filterable/datable.rb Filterable::Datable#bounds 38 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#column_errors 59 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#scope_errors 76, 79 manually dispatches method call
ManualDispatch lib/filterable/equatable.rb Filterable::Equatable#values 59 manually dispatches method call
ManualDispatch lib/filterable/rangeable.rb Filterable::Rangeable#bounds 36 manually dispatches method call
ManualDispatch lib/filterable/scopable.rb Filterable::Scopable#accepted 47, 50 manually dispatches method call
ManualDispatch lib/filterable/togglable.rb Filterable::Togglable#accepted 54, 56 manually dispatches method call
NestedIterators lib/filterable/datable.rb Filterable::Datable#accepted 55 contains iterators nested 2 deep
NestedIterators lib/filterable/rangeable.rb Filterable::Rangeable#accepted 53 contains iterators nested 2 deep
NilCheck lib/filterable/equatable.rb Filterable::Equatable#accepted 46 performs a nil-check
NilCheck lib/filterable/scopable.rb Filterable::Scopable#accepted 50 performs a nil-check
NilCheck lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 70 performs a nil-check
TooManyStatements lib/filterable/concern.rb Filterable::Concern#applied_filters 65 has approx 6 statements
TooManyStatements lib/filterable/datable.rb Filterable::Datable#accepted 52 has approx 6 statements
TooManyStatements lib/filterable/rangeable.rb Filterable::Rangeable#accepted 50 has approx 6 statements
TooManyStatements lib/filterable/scopable.rb Filterable::Scopable#accepted 44 has approx 7 statements
TooManyStatements lib/filterable/sortable.rb Filterable::Sortable#terms 44 has approx 6 statements
TooManyStatements lib/filterable/togglable.rb Filterable::Togglable#accepted 51 has approx 6 statements
TooManyStatements lib/filterable/value_normalization.rb Filterable::ValueNormalization#normalize 15 has approx 6 statements
UtilityFunction lib/filterable/concern.rb Filterable::Concern#indifferent? 91 doesn't depend on instance state (maybe move it to another class?)
📊 Flog complexity breakdown
577.7: flog total
     7.0: flog/method average

    28.7: main#none
    21.0: Filterable::Concern#applied_filters lib/filterable/concern.rb:65-72
    18.8: Filterable::Railtie::initializer#filterable.active_record lib/filterable/railtie.rb:19-29
    16.3: Filterable::Concern#filterable   lib/filterable/concern.rb:47-52
    15.8: Filterable::Scopable#accepted    lib/filterable/scopable.rb:44-54
    15.7: Filterable::Datable::Range#call  lib/filterable/datable/range.rb:17-23
    12.8: Filterable::Togglable#accepted   lib/filterable/togglable.rb:51-60
    12.8: Filterable::Sortable#terms       lib/filterable/sortable.rb:44-53
    11.2: Filterable::Rangeable::Minimum#call lib/filterable/rangeable/minimum.rb:16-21
    11.2: Filterable::Rangeable::Maximum#call lib/filterable/rangeable/maximum.rb:16-21
    11.2: Filterable::Datable::Since#call  lib/filterable/datable/since.rb:16-21
    11.2: Filterable::Datable::Before#call lib/filterable/datable/before.rb:16-21
    11.2: Filterable::Datable::After#call  lib/filterable/datable/after.rb:16-21
    10.4: Filterable::Rangeable#accepted   lib/filterable/rangeable.rb:50-58
    10.4: Filterable::Datable#accepted     lib/filterable/datable.rb:52-60
    10.3: Filterable::ValueNormalization#normalize lib/filterable/value_normalization.rb:15-25
     9.0: Filterable::DeclarationsValidator#column_errors lib/filterable/declarations_validator.rb:57-66
     9.0: Filterable::Concerns::Datable#none
     8.8: Filterable::Concern#add_filter   lib/filterable/concern.rb:30-38
     8.6: Filterable::Concerns::Rangeable#rangeable_attribute_names lib/filterable/concerns/rangeable.rb:24-31
     8.6: Filterable::Concerns::Datable#datable_attribute_names lib/filterable/concerns/datable.rb:25-32
     8.6: Filterable::Concerns::Scopable#scopable_scope_names lib/filterable/concerns/scopable.rb:22-29
     8.6: Filterable::Concerns::Sortable#sortable_attribute_names lib/filterable/concerns/sortable.rb:22-29
     8.6: Filterable::Concerns::Equatable#equatable_attribute_names lib/filterable/concerns/equatable.rb:23-30
     8.6: Filterable::Concerns::Togglable#togglable_scope_names lib/filterable/concerns/togglable.rb:23-30
     8.3: Filterable::Sortable#call        lib/filterable/sortable.rb:19-25
     8.3: Filterable::Rangeable#bounds     lib/filterable/rangeable.rb:34-38
     8.3: Filterable::Datable#bounds       lib/filterable/datable.rb:36-40
     8.3: Filterable::Datable::Since#applied lib/filterable/datable/since.rb:29-34
     8.3: Filterable::Datable::Range#applied lib/filterable/datable/range.rb:31-36
📊 Flay duplication breakdown
Total score (lower is better) = 521

1) Similar code found in :module (mass = 245)
  lib/filterable/datable/after.rb:4
  lib/filterable/datable/before.rb:4
  lib/filterable/datable/since.rb:4
  lib/filterable/rangeable/maximum.rb:4
  lib/filterable/rangeable/minimum.rb:4

2) Similar code found in :module (mass = 164)
  lib/filterable/concerns/equatable.rb:4
  lib/filterable/concerns/scopable.rb:4
  lib/filterable/concerns/sortable.rb:4
  lib/filterable/concerns/togglable.rb:4

3) Similar code found in :defn (mass = 66)
  lib/filterable/datable.rb:52
  lib/filterable/rangeable.rb:50

4) Similar code found in :defn (mass = 46)
  lib/filterable/datable.rb:36
  lib/filterable/rangeable.rb:34

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ Coverage Report: 100.0% (0.0%)

████████████████████ 100.0% — 394/394 lines covered

Base PR Δ
Total 100.0% 100.0% ±0%

All changes are tested.

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ RuboCop: 0 offenses (0)

Base PR Δ
Offenses 0 0 ±0

No RuboCop offenses.

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ Documentation Quality: 100.0% (+0.5%)

████████████████████ 100.0% — 750/750 checks passing

Base PR Δ
Coverage 99.5% 100.0% 🟢 +0.5%
Failing 3 0 🟢 -3

Documentation quality above threshold.

jplot added 2 commits July 3, 2026 13:31
- `?filters[priced]` and `filters[priced]=` now switch the toggle on,
  so a hand-written URL does not need an explicit value
- explicit false values ('false', '0' — what Rails' check_box hidden
  field submits) and arbitrary strings still narrow nothing, keeping
  form-submitted checkboxes correct
- summary and @api tag were missing since #10; yardstick flags it on
  every PR touching the file, zero measurements fail with the project
  config now
@jplot
jplot merged commit 99a517f into main Jul 3, 2026
12 checks passed
@jplot
jplot deleted the feat/scopable branch July 3, 2026 11:45
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