@@ -70,14 +70,21 @@ def default_filters(defaults)
7070 # nested +filters+ params merged over the declared defaults. Returns +all+
7171 # untouched when neither carries anything usable.
7272 #
73+ # The optional context hash is handed, as given, to every filter whose
74+ # callable accepts a third argument — +Model.filterable(params, user_id:
75+ # current_user.id)+ — so a custom filter can depend on the caller without
76+ # global state. Two-argument filters, the built-in ones included, never
77+ # see it.
78+ #
7379 # @api public
7480 # @param params [Hash, ActionController::Parameters] request params, read at +params[:filters]+.
81+ # @param context [Hash] caller-provided context for context-aware filters.
7582 # @return [ActiveRecord::Relation] the filtered relation.
76- def filterable ( params = { } )
83+ def filterable ( params = { } , context = { } )
7784 filters_params = effective_filters ( params )
7885 return all if filters_params . empty?
7986
80- filters . reduce ( all ) { |scope , filter | filter . call ( filters_params , scope ) || scope }
87+ filters . reduce ( all ) { |scope , filter | apply_filter ( filter , filters_params , scope , context ) || scope }
8188 end
8289
8390 # Reports which filters {filterable} would actually apply from the same
@@ -90,14 +97,15 @@ def filterable(params = {})
9097 #
9198 # @api public
9299 # @param params [Hash, ActionController::Parameters] request params, read at +params[:filters]+.
100+ # @param context [Hash] caller-provided context for context-aware filters.
93101 # @return [ActiveSupport::HashWithIndifferentAccess] public name => accepted value.
94- def applied_filters ( params = { } )
102+ def applied_filters ( params = { } , context = { } )
95103 filters_params = effective_filters ( params )
96104 report = { } . with_indifferent_access
97105 return report if filters_params . empty?
98106
99107 filters . select { |filter | filter . respond_to? ( :applied ) }
100- . reduce ( report ) { |acc , filter | acc . deep_merge ( filter . applied ( filters_params , all ) ) }
108+ . reduce ( report ) { |acc , filter | acc . deep_merge ( report_from ( filter , filters_params , context ) ) }
101109 end
102110
103111 # A validator checking every declaration against the model's columns, meant
@@ -111,6 +119,52 @@ def filterable_declarations
111119
112120 private
113121
122+ # Calls one filter, handing it the context when its +call+ accepts a
123+ # third argument.
124+ #
125+ # @api private
126+ # @param filter [#call] the registered filter.
127+ # @param filters_params [ActiveSupport::HashWithIndifferentAccess] the effective +filters+ params.
128+ # @param scope [ActiveRecord::Relation] the relation being narrowed.
129+ # @param context [Hash] the caller-provided context.
130+ # @return [ActiveRecord::Relation, nil] the filter's return value.
131+ def apply_filter ( filter , filters_params , scope , context )
132+ if context_aware? ( filter , :call )
133+ filter . call ( filters_params , scope , context )
134+ else
135+ filter . call ( filters_params , scope )
136+ end
137+ end
138+
139+ # Collects one filter's report, handing it the context when its +applied+
140+ # accepts a third argument.
141+ #
142+ # @api private
143+ # @param filter [#applied] the registered filter.
144+ # @param filters_params [ActiveSupport::HashWithIndifferentAccess] the effective +filters+ params.
145+ # @param context [Hash] the caller-provided context.
146+ # @return [Hash] the filter's report.
147+ def report_from ( filter , filters_params , context )
148+ if context_aware? ( filter , :applied )
149+ filter . applied ( filters_params , all , context )
150+ else
151+ filter . applied ( filters_params , all )
152+ end
153+ end
154+
155+ # Whether a filter's callable accepts a third positional argument — an
156+ # exact three-parameter signature, or a negative arity whose required
157+ # parameters leave room for one (optional argument or splat).
158+ #
159+ # @api private
160+ # @param filter [Object] the registered filter.
161+ # @param method_name [Symbol] +:call+ or +:applied+.
162+ # @return [Boolean]
163+ def context_aware? ( filter , method_name )
164+ arity = filter . is_a? ( Proc ) ? filter . arity : filter . method ( method_name ) . arity
165+ arity == 3 || ( arity . negative? && arity >= -4 )
166+ end
167+
114168 # The request filters merged over the evaluated defaults — the single
115169 # params shape handed to every filter by {filterable} and
116170 # {applied_filters}, so the two cannot drift. A key present in the
0 commit comments