File Coverage

blib/lib/Params/Filter.pm
Criterion Covered Total %
statement 110 114 96.4
branch 57 68 83.8
condition 23 28 82.1
subroutine 10 10 100.0
pod 8 8 100.0
total 208 228 91.2


line stmt bran cond sub pod time code
1             package Params::Filter;
2 4     4   707697 use v5.36;
  4         15  
3             our $VERSION = '0.006_002';
4              
5             =head1 NAME
6              
7             Params::Filter - Fast field filtering for parameter construction
8              
9             =head1 SYNOPSIS
10              
11             use Params::Filter;
12              
13             # Define filter rules
14             my @required_fields = qw(name email);
15             my @accepted_fields = qw(phone city state zip);
16             my @excluded_fields = qw(ssn password);
17              
18             # Functional interface
19             # Apply filter to incoming data (from web form, CLI, API, etc.)
20             my ($filtered_data, $status) = filter(
21             $incoming_params, # Data from external source
22             \@required_fields,
23             \@accepted_fields,
24             \@excluded_fields,
25             );
26              
27             if ($filtered_data) {
28             # Success - use filtered data
29             process_user($filtered_data);
30             } else {
31             # Error - missing required fields
32             die "Validation failed: $status";
33             }
34              
35             # Object-oriented interface
36             my $user_filter = Params::Filter->new_filter({
37             required => ['username', 'email'],
38             accepted => ['first_name', 'last_name', 'phone', 'bio'],
39             excluded => ['password', 'ssn', 'credit_card'],
40             });
41              
42             # Apply same filter to multiple incoming datasets
43             my ($user1, $msg1) = $user_filter->apply($web_form_data);
44             my ($user2, $msg2) = $user_filter->apply($api_request_data);
45             my ($user3, $msg3) = $user_filter->apply($db_record_data);
46              
47             =head1 DESCRIPTION
48              
49             C provides fast, lightweight parameter filtering that
50             checks only for the presence or absence of specified fields. It does B
51             validate values - no type checking, truthiness testing, or lookups.
52              
53             This module separates field filtering from value validation:
54              
55             =over 4
56              
57             =item * **Field filtering** (this module) - Check which fields are present/absent
58              
59             =item * **Value validation** (later step) - Check if field values are correct
60              
61             =back
62              
63             This approach handles common parameter issues:
64              
65             =over 4
66              
67             =item * Subroutine signatures can become unwieldy with many parameters
68              
69             =item * Ad-hoc argument checking is error-prone
70              
71             =item * Validation may not catch missing inputs quickly enough
72              
73             =item * The number of fields to check multiplies validation time
74              
75             =back
76              
77             =head2 When to Use This Module
78              
79             This module is useful when you have:
80              
81             =over 4
82              
83             =item * Pre-defined filter rules (from config files, constants, database schemas)
84              
85             =item * Known downstream input or process parameters (for APIs, method/subroutine arguments, database operations)
86              
87             =item * Incoming data from differing sources (web forms, APIs, databases, user input)
88              
89             =item * No guarantee that incoming data is consistent or complete
90              
91             =item * Need to process multiple datasets with the same rules
92              
93             =item * Want to reject unwanted fields before value validation
94              
95             =back
96              
97             =head2 When NOT to Use This Module
98              
99             If you're constructing both the filter rules B the data structure at the
100             same point in your code, you probably don't need this module except
101             during development or debugging. The module's expected use is
102             to apply pre-defined rules to data that may be inconsistent or
103             incomplete for its intended use. If there isn't repetition
104             or an unreliable data structure, this might be overkill.
105              
106             =cut
107              
108             =head2 This Module Does NOT Do Fancy Stuff
109              
110             As much as this module attempts to be versatile in usage, there are some
111             B
112              
113             =over 4
114              
115             =item * No regex field name matching for designating fields to require, accept, or exclude
116              
117             =item * No conditional field designations within a filter:
118             C.
119             But see C, C, C,
120             as ways to adjust a filter's behavior - or just have alternative filters.
121              
122             =item * No coderefs or callbacks for use when filtering
123              
124             =item * No substitutions or changes to field names
125              
126             =item * No built-in filter lists except null [] = none
127              
128             =item * No fields B to data, EXCEPT:
129              
130             =over8
131              
132             * If the provided data resolves to a list or array with an odd number of elements,
133             the LAST element is treated as a flag, set to the value 1
134              
135             * If the provided data resolves to a single non-reference scalar (probably a text string)
136             the data is returned as a hashref value with the key ‘_’
137              
138             =back
139              
140             =back
141              
142             =cut
143              
144 4     4   28 use Exporter;
  4         7  
  4         6699  
145             our @ISA = qw{ Exporter };
146             our @EXPORT = qw{ filter };
147              
148             sub new_filter {
149 40     40 1 723193 my ($class,$args) = @_;
150 40 100 66     220 $args = {} unless ($args and ref($args) =~ /hash/i);
151             my $self = {
152             required => $args->{required} || [],
153             accepted => $args->{accepted} || [],
154             excluded => $args->{excluded} || [],
155 40   100     438 debug => $args->{DEBUG} || $args->{debug} || 0,
      100        
      100        
      100        
156             };
157 40         74 bless $self, __PACKAGE__;
158 40         107 return $self;
159             }
160              
161             =head1 OBJECT-ORIENTED INTERFACE
162              
163             =head2 new_filter
164              
165             my $filter = Params::Filter->new_filter({
166             required => ['field1', 'field2'],
167             accepted => ['field3', 'field4', 'field5'],
168             excluded => ['forbidden_field'],
169             DEBUG => 1, # Optional debug mode
170             });
171              
172             # Empty constructor - rejects all fields by default
173             my $strict_filter = Params::Filter->new_filter();
174              
175             Creates a reusable filter object with predefined field rules. The filter
176             can then be applied to multiple datasets using the L method.
177              
178             =head3 Parameters
179              
180             =over 4
181              
182             =item * C - Arrayref of names of required fields (default: [])
183              
184             =item * C - Arrayref of names of optional fields (default: [])
185              
186             =item * C - Arrayref of names of fields to always remove (default: [])
187              
188             =item * C - Boolean to enable debug warnings (default: 0)
189              
190             =back
191              
192             =head3 Returns
193              
194             A C object
195              
196             =head3 Example
197              
198             # Create filter for user registration data
199             my $user_filter = Params::Filter->new_filter({
200             required => ['username', 'email'],
201             accepted => ['first_name', 'last_name', 'phone', 'bio'],
202             excluded => ['password', 'ssn', 'credit_card'],
203             });
204              
205             # Apply to multiple incoming datasets
206             my ($user1, $msg1) = $user_filter->apply($web_form_data);
207             my ($user2, $msg2) = $user_filter->apply($api_request_data);
208              
209             =head2 apply
210              
211             my ($filtered, $status) = $filter->apply($input_data);
212              
213             Applies the filter's predefined rules to input data. This is the OO
214             equivalent of the L function.
215              
216             =head3 Parameters
217              
218             =over 4
219              
220             =item * C<$input_data> - Hashref, arrayref, or scalar to filter
221              
222             =back
223              
224             =head3 Returns
225              
226             In list context: C<(hashref, status_message)> or C<(undef, error_message)>
227              
228             In scalar context: Hashref with filtered parameters, or C on failure
229              
230             =head3 Example
231              
232             my $filter = Params::Filter->new_filter({
233             required => ['id', 'type'],
234             accepted => ['name', 'value'],
235             });
236              
237             # Process multiple records from database
238             for my $record (@db_records) {
239             my ($filtered, $msg) = $filter->apply($record);
240             if ($filtered) {
241             process_record($filtered);
242             } else {
243             log_error("Record failed: $msg");
244             }
245             }
246              
247             =cut
248              
249             sub set_required {
250 22     22 1 112 my ($self, @fields) = @_;
251 22 100       72 @fields = ref $fields[0] eq 'ARRAY' ? $fields[0]->@* : @fields;
252 22         36 my @required = grep { defined } @fields;
  28         68  
253 22 100       82 $self->{required} = @required ? [ @required ] : [];
254 22         83 return $self;
255             }
256              
257             sub set_accepted {
258 9     9 1 269 my ($self, @fields) = @_;
259 9 100       27 @fields = ref $fields[0] eq 'ARRAY' ? $fields[0]->@* : @fields;
260 9         16 my @accepted = grep { defined } @fields;
  17         30  
261 9 100       26 $self->{accepted} = @accepted ? [ @accepted ] : [];
262 9         27 return $self;
263             }
264              
265             sub accept_all {
266 9     9 1 16 my ($self) = @_;
267 9         22 $self->{accepted} = ['*'];
268 9         51 return $self;
269             }
270              
271             sub accept_none {
272 4     4 1 8 my ($self) = @_;
273 4         8 $self->{accepted} = [];
274 4         12 return $self;
275             }
276              
277             sub set_excluded {
278 10     10 1 917 my ($self, @fields) = @_;
279 10 100       30 @fields = ref $fields[0] eq 'ARRAY' ? $fields[0]->@* : @fields;
280 10         39 my @excluded = grep { defined } @fields;
  14         32  
281 10 100       25 $self->{excluded} = @excluded ? [ @excluded ] : [];
282 10         24 return $self;
283             }
284              
285             sub apply {
286 40     40 1 167 my ($self,$args) = @_;
287 40   50     88 my $req = $self->{required} || [];
288 40   50     76 my $ok = $self->{accepted} || [];
289 40   50     75 my $no = $self->{excluded} || [];
290 40   100     135 my $db = $self->{debug} || 0;
291 40         84 my @result = filter( $args, $req, $ok, $no, $db);
292 40 100       146 return wantarray ? @result : $result[0];
293             }
294              
295             =head1 MODIFIER METHODS
296              
297             Modifier methods allow dynamic configuration of filter rules after creation of the filter object.
298             All methods return C<$self> for method chaining.
299             A filter may call its modifier methods more than once, and the changes take effect immediately.
300              
301             =head2 set_required
302              
303             $filter->set_required(['id', 'name', 'email']); # Arrayref
304             $filter->set_required('id', 'name', 'email'); # List
305             $filter->set_required(); # Clears to []
306              
307             Sets the required field names. Accepts either an arrayref or a list of
308             field names. Calling with no arguments sets required to empty array.
309              
310             =head2 set_accepted
311              
312             $filter->set_accepted(['phone', 'city']); # Arrayref
313             $filter->set_accepted('phone', 'city'); # List
314             $filter->set_accepted(); # Clears to []
315             $filter->set_accepted(['*']); # Accept all (except excluded)
316              
317             Sets the optional (accepted) field names. Accepts either an arrayref or a
318             list of field names. Calling with no arguments sets accepted to empty array.
319              
320             =head2 set_excluded
321              
322             $filter->set_excluded(['password', 'ssn']); # Arrayref
323             $filter->set_excluded('password', 'ssn'); # List
324             $filter->set_excluded(); # Clears to []
325              
326             Sets the excluded field names (fields to always remove). Accepts either an
327             arrayref or a list of field names. Calling with no arguments sets excluded
328             to empty array.
329              
330             =head2 accept_all
331              
332             $filter->accept_all(); # Sets accepted to ['*']
333              
334             Convenience method that sets accepted fields to C<['*']> (wildcard mode),
335             allowing all fields except those in excluded.
336              
337             =head2 accept_none
338              
339             $filter->accept_none(); # Sets accepted to []
340              
341             Convenience method that sets accepted fields to C<[]> (empty array),
342             allowing only required fields.
343              
344             =head3 Modifier Method Examples
345              
346             # Method chaining for one-liner configuration
347             my $filter = Params::Filter->new_filter();
348             # When needed:
349             $filter->set_required(['id', 'name'])
350             ->set_accepted(['email', 'phone'])
351             ->set_excluded(['password']);
352              
353             # Environment-based configuration
354             my $filter = Params::Filter->new_filter();
355              
356             if ($ENV{MODE} eq 'production') {
357             $filter->set_required(['api_key'])
358             ->set_accepted(['timeout', 'retries'])
359             ->set_excluded(['debug_info']);
360             } else {
361             $filter->set_required(['debug_mode'])
362             ->accept_all();
363             }
364              
365             # Dynamic configuration from config file
366             if ( $DEBUG ) {
367             my $db_config = load_config('debug_fields.json');
368             $filter->set_required($db_config->{required})
369             ->set_accepted($db_config->{accepted})
370             ->set_excluded($db_config->{excluded});
371             }
372              
373             =head1 FUNCTIONAL INTERFACE
374              
375             =head2 filter
376              
377             my ($filtered, $status) = filter(
378             $input_data, # Hashref, arrayref, or scalar
379             \@required, # Arrayref of required field names
380             \@accepted, # Arrayref of optional field names (default: [])
381             \@excluded, # Arrayref of names of fields to remove (default: [])
382             $debug_mode, # Boolean: enable warnings (default: 0)
383             );
384              
385             # Scalar context - returns filtered hashref or undef on failure
386             my $result = filter($input, \@required, \@accepted);
387              
388             Filters input data according to field specifications. Only checks for
389             presence/absence of fields, not field values.
390              
391             =head3 Parameters
392              
393             =over 4
394              
395             =item * C<$input_data> - Input parameters (hashref, arrayref, or scalar)
396              
397             =item * C<\@required> - Arrayref of names of fields that B be present
398              
399             =item * C<\@accepted> - Arrayref of optional names of fields to accept (default: [])
400              
401             =item * C<\@excluded> - Arrayref of names of fields to remove even if accepted (default: [])
402              
403             =item * C<$debug_mode> - Boolean to enable warnings (default: 0)
404              
405             =back
406              
407             =head3 Returns
408              
409             In list context: C<(hashref, status_message)> or C<(undef, error_message)>
410              
411             In scalar context: Hashref with filtered parameters, or C on failure
412              
413             =head3 Example
414              
415             # Define filter rules (could be from config file)
416             my @required = qw(username email);
417             my @accepted = qw(full_name phone);
418             my @excluded = qw(password ssn);
419              
420             # Apply to incoming data from web form
421             my ($user_data, $msg) = filter(
422             $form_submission,
423             \@required,
424             \@accepted,
425             \@excluded,
426             );
427              
428             if ($user_data) {
429             create_user($user_data);
430             } else {
431             log_error($msg);
432             }
433              
434             =cut
435              
436 57     57 1 177409 sub filter ($args,$req,$ok=[],$no=[],$db=0) {
  57         76  
  57         66  
  57         100  
  57         71  
  57         66  
  57         78  
437 57         82 my %args = ();
438 57         77 my @messages = (); # Parsing messages (always reported)
439 57         71 my @warnings = (); # Debug warnings (only when $db is true)
440              
441 57 100       119 if (ref $args eq 'HASH') {
    100          
    50          
442 50         153 %args = $args->%*
443             }
444             elsif (ref $args eq 'ARRAY') {
445 5 100       13 if (ref($args->[0]) eq 'HASH') {
446 1         6 %args = $args->[0]->%*; # Ignore the rest
447             }
448             else {
449 4         9 my @args = $args->@*;
450 4 100       14 if (@args == 1) {
    100          
451 1         3 %args = ( '_' => $args[0] ); # make it a value with key '_'
452 1 50       3 my $preview = length($args[0]) > 20
453             ? substr($args[0], 0, 20) . '...'
454             : $args[0];
455 1         4 push @messages => "Plain text argument accepted with key '_': '$preview'";
456             }
457             elsif ( @args % 2 ) {
458 2         6 %args = (@args, 1); # make last arg element a flag
459 2         8 push @messages => "Odd number of arguments provided; " .
460             "last element '$args[-1]' converted to flag with value 1";
461             }
462             else {
463 1         4 %args = @args; # turn array into hash pairs
464             }
465             }
466             }
467             elsif ( !ref $args ) {
468 2         7 %args = ( '_' => $args); # make it a value with key '_'
469 2 50       5 my $preview = length($args) > 20
470             ? substr($args, 0, 20) . '...'
471             : $args;
472 2         6 push @messages => "Plain text argument accepted with key '_': '$preview'";
473             }
474              
475 57         119 my @required_flds = $req->@*;
476 57 100       120 unless ( keys %args ) {
477             my $err = "Unable to initialize without required arguments: " .
478 1         2 join ', ' => map { "'$_'" } @required_flds;
  1         5  
479 1 50       6 return wantarray ? (undef, $err) : undef;
480             }
481              
482 56 100       118 if ( scalar keys(%args) < @required_flds ) {
483             my $err = "Unable to initialize without all required arguments: " .
484 2         8 join ', ' => map { "'$_'" } @required_flds;
  4         15  
485 2 50       18 return wantarray ? (undef, $err) : undef;
486             }
487              
488             # Now create the output hashref
489 54         102 my $filtered = {};
490              
491             # Check for each required field
492 54         65 my @missing_required;
493 54         72 my $used_keys = 0;
494 54         89 for my $fld (@required_flds) {
495 60 50       131 if ( exists $args{$fld} ) {
496 60         132 $filtered->{$fld} = delete $args{$fld};
497 60         109 $used_keys++;
498             }
499             else {
500 0         0 push @missing_required => $fld;
501             }
502             }
503             # Return fast if all set
504             # required fields assured and no other fields provided
505 54 100       99 if ( keys(%args) == 0 ) {
506 10 100       47 return wantarray ? ($filtered, "Admitted") : $filtered;
507             }
508             # required fields assured and no more fields allowed
509 44 100 66     190 if ( scalar keys $filtered->%* == @required_flds and not $ok->@*) {
510 5 50       30 return wantarray ? ($filtered, "Admitted") : $filtered;
511             }
512             # Can't continue
513 39 50       71 if ( @missing_required ) {
514             my $err = "Unable to initialize without required arguments: " .
515 0         0 join ', ' => map { "'$_'" } @missing_required;
  0         0  
516 0 0       0 return wantarray ? (undef, $err) : undef;
517             }
518              
519             # Now remove any excluded fields
520 39         44 my @excluded;
521 39         93 for my $fld ($no->@*) {
522 20 100       44 if ( exists $args{$fld} ) {
523 18         26 delete $args{$fld};
524 18         52 push @excluded => $fld;
525             }
526             }
527              
528             # Check if wildcard '*' appears in accepted list
529 39         62 my $has_wildcard = grep { $_ eq '*' } $ok->@*;
  54         129  
530              
531 39 100       68 if ($has_wildcard) {
532             # Wildcard present: accept all remaining fields
533 13         33 for my $fld (keys %args) {
534 22         49 $filtered->{$fld} = delete $args{$fld};
535             }
536             }
537             else {
538             # Track but don't include if not on @accepted list
539 26         41 for my $fld ($ok->@*) {
540 41 100       75 if ( exists $args{$fld} ) {
541 35         77 $filtered->{$fld} = delete $args{$fld};
542             }
543             }
544             }
545              
546 39         65 my @unrecognized = keys %args; # Everything left
547 39 100 100     108 if ( $db and @unrecognized > 0 ) {
548             push @warnings => "Ignoring unrecognized arguments: " .
549 3         8 join ', ' => map { "'$_'" } @unrecognized;
  4         42  
550             }
551 39 100 100     87 if ( $db and @excluded > 0 ) {
552             push @warnings => "Ignoring excluded arguments: " .
553 3         5 join ', ' => map { "'$_'" } @excluded;
  3         10  
554             }
555              
556             # Combine parsing messages (always) with debug warnings (if debug mode)
557 39         60 my @all_msgs = (@messages, @warnings);
558 39 100       80 my $return_msg = @all_msgs
559             ? join "\n" => @all_msgs
560             : "Admitted";
561              
562 39 50       160 return wantarray ? ( $filtered, $return_msg ) : $filtered;
563             }
564              
565             =head1 RETURN VALUES
566              
567             Both L and L return different values depending on context:
568              
569             =head2 Success
570              
571             =over 4
572              
573             =item * List context: C<(hashref, "Admitted")> or C<(hashref, warning_message)>
574              
575             =item * Scalar context: Hashref with filtered parameters
576              
577             =back
578              
579             =head2 Failure
580              
581             =over 4
582              
583             =item * List context: C<(undef, error_message)>
584              
585             =item * Scalar context: C
586              
587             =back
588              
589             =head2 Common Status Messages
590              
591             =over 4
592              
593             =item * "Admitted" - All required fields present, filtering successful
594              
595             =item * "Plain text argument accepted with key '_': '...'" - Parsing message (always shown)
596              
597             =item * "Odd number of arguments provided; last element 'X' converted to flag with value 1" - Parsing message (always shown)
598              
599             =item * "Ignoring excluded arguments: 'field1', 'field2'..." - Debug message (debug mode only)
600              
601             =item * "Ignoring unrecognized arguments: 'field1', 'field2'..." - Debug message (debug mode only)
602              
603             =item * "Unable to initialize without required arguments: 'field1', 'field2'..." - Error
604              
605             =back
606              
607             =head1 FEATURES
608              
609             =over 4
610              
611             =item * **Dual interface** - Functional or OO usage
612              
613             =item * **Fast-fail** - Returns immediately on missing required parameters
614              
615             =item * **Fast-success** - Returns immediately if all required parameters are provided and no others are provided or will be accepted
616              
617             =item * **Flexible input** - Accepts hashrefs, arrayrefs, or scalars
618              
619             =item * **Wildcard support** - Use C<'*'> in accepted list to accept all fields
620              
621             =item * **No value checking** - Only presence/absence of fields
622              
623             =item * **Debug mode** - Optional warnings about unrecognized or excluded fields
624              
625             =item * **Method chaining** - Modifier methods return C<$self>
626              
627             =item * **Perl 5.40+** - Modern Perl with signatures and post-deref
628              
629             =item * **No dependencies** - Only core Perl's L
630              
631             =back
632              
633             =head1 DEBUG MODE
634              
635             Debug mode provides additional information about field filtering during development:
636              
637             my ($filtered, $msg) = filter(
638             $input,
639             ['name'],
640             ['email'],
641             ['password'],
642             1, # Enable debug mode
643             );
644              
645             Debug warnings (only shown when debug mode is enabled):
646              
647             =over 4
648              
649             =item * Excluded fields that were removed
650              
651             =item * Unrecognized fields that were ignored
652              
653             =back
654              
655             Parsing messages (always shown, regardless of debug mode):
656              
657             =over 4
658              
659             =item * Plain text arguments accepted with key '_'
660              
661             =item * Odd number of array elements converted to flags
662              
663             =back
664              
665             Parsing messages inform you about transformations the filter made to your input format.
666             These are always reported because they affect the structure of the returned data.
667             Debug warnings help you understand which fields were filtered out during development.
668              
669             =head1 WILDCARD SUPPORT
670              
671             The C parameter supports a wildcard C<'*'> to accept all fields
672             (except those in C).
673              
674             =head2 Wildcard Usage
675              
676             # Accept all fields
677             filter($input, [], ['*']);
678              
679             # Accept all except specific exclusions
680             filter($input, [], ['*'], ['password', 'ssn']);
681              
682             # Required + all other fields
683             filter($input, ['id', 'name'], ['*']);
684              
685             =head2 Important Notes
686              
687             =over 4
688              
689             =item * C<'*'> is B parameter>
690              
691             =item * In C or C, C<'*'> is treated as a literal field name
692              
693             =item * Empty C<[]> for accepted means "accept none beyond required"
694              
695             =item * Multiple wildcards are redundant but harmless
696              
697             =item * Exclusions are always removed before acceptance is processed
698              
699             =back
700              
701             =head2 Debugging Pattern
702              
703             A common debugging pattern is to add C<'*'> to an existing accepted list:
704              
705             # Normal operation
706             filter($input, ['id'], ['name', 'email']);
707              
708             # Debugging - see all inputs
709             filter($input, ['id'], ['name', 'email', '*']);
710              
711             Or, start with minimum to troubleshoot specific fields
712              
713             filter($input, ['id'], []);
714              
715             # then
716             filter($input, ['id'], ['name']);
717              
718             # then
719             filter($input, ['id'], ['email']);
720              
721             # then
722             filter($input, ['id'], ['name', 'email']);
723              
724             # then
725             filter($input, ['id'], ['*']);
726              
727              
728             =head1 EXAMPLES
729              
730             =head2 Basic Form Validation
731              
732             use Params::Filter; # auto-imports filter() subroutine
733              
734             # Define filtering rules (could be from config file)
735             my @required = qw(name email);
736             my @accepted = qw(phone city state zip);
737              
738             # Apply to incoming web form data
739             my ($user_data, $status) = filter(
740             $form_submission, # Data from web form
741             \@required,
742             \@accepted,
743             );
744              
745             if ($user_data) {
746             register_user($user_data);
747             } else {
748             show_error($status);
749             }
750              
751             =head2 Reusable Filter for Multiple Data Sources
752              
753             # Create filter once
754             my $user_filter = Params::Filter->new_filter({
755             required => ['username', 'email'],
756             accepted => ['full_name', 'phone', 'bio'],
757             excluded => ['password', 'ssn', 'credit_card'],
758             });
759              
760             # Apply to multiple incoming datasets
761             my ($user1, $msg1) = $user_filter->apply($web_form_data);
762             my ($user2, $msg2) = $user_filter->apply($api_request_data);
763             my ($user3, $msg3) = $user_filter->apply($csv_import_data);
764              
765             =head2 Environment-Specific Filtering
766              
767             my $filter = Params::Filter->new_filter();
768              
769             if ($ENV{APP_MODE} eq 'production') {
770             # Strict: only specific fields allowed
771             $filter->set_required(['api_key'])
772             ->set_accepted(['timeout', 'retries'])
773             ->set_excluded(['debug_info', 'verbose']);
774             } else {
775             # Development: allow everything
776             $filter->set_required(['debug_mode'])
777             ->accept_all();
778             }
779              
780             my ($config, $msg) = $filter->apply($incoming_config);
781              
782             =head2 Security Filtering
783              
784             # Remove sensitive fields from user input
785             my ($safe_data, $msg) = filter(
786             $user_input,
787             ['username', 'email'], # required
788             ['full_name', 'phone', 'bio'], # accepted
789             ['password', 'ssn', 'api_key'], # excluded
790             );
791              
792             # Result contains only safe fields
793             # password, ssn, api_key are removed even if provided
794              
795             =head2 Dynamic Configuration from File
796              
797             # Load filter rules from config file
798             my $config = decode_json(`cat filters.json`);
799              
800             my $filter = Params::Filter->new_filter()
801             ->set_required($config->{user_create}{required})
802             ->set_accepted($config->{user_create}{accepted})
803             ->set_excluded($config->{user_create}{excluded});
804              
805             # Apply to incoming data
806             my ($filtered, $msg) = $filter->apply($api_data);
807              
808             =head2 Data Segregation for Multiple Subsystems
809              
810             A common pattern is splitting incoming data into subsets for different
811             handlers or storage locations. Each filter extracts only the fields needed
812             for its specific purpose, implementing security through compartmentalization.
813              
814             # Main subscription form collects:
815             # name, email, zip,
816             # user_id, password, credit_card_number, subscription_term
817              
818             # Subscriber profile form collects:
819             # name, email, address, city, state, zip,
820             # user_id, password, credit_card_number,
821             # phone, occupation, position, education
822             # alt_card_number, billing_address, billing_zip
823              
824             # Promo subscription form collects:
825             # name, email, zip, subscription_term,
826             # user_id, password, credit_card_number, promo_code
827              
828             my $data = $webform->input(); # From any of the above
829              
830             # Filters
831             # Personal data - general user info (no sensitive data)
832             my $person_filter = Params::Filter->new_filter({
833             required => ['name', 'user_id', 'email'],
834             accepted => ['address', 'city', 'state', 'zip', 'phone', 'occupation', 'position', 'education'],
835             excluded => ['password', 'credit_card_number'],
836             });
837              
838             # Business data - subscription and billing info
839             my $biz_filter = Params::Filter->new_filter({
840             required => ['user_id', 'subscription_term', 'credit_card_number', 'zip'],
841             accepted => ['alt_card_number', 'billing_address', 'billing_zip', 'promo_code'],
842             excluded => ['password'],
843             });
844              
845             # Authentication data - only credentials
846             my $auth_filter = Params::Filter->new_filter({
847             required => ['user_id', 'password'],
848             accepted => [],
849             excluded => [],
850             });
851              
852             # Apply all filters to the same web form submission
853             my ($person_data, $pmsg) = $person_filter->apply($data);
854             my ($biz_data, $bmsg) = $biz_filter->apply($data);
855             my ($auth_data, $amsg) = $auth_filter->apply($data);
856              
857             unless ($person_data && $biz_data && $auth_data) {
858             return "Unable to add user: " .
859             join ' ' => grep { $_ ne 'Admitted' } ($pmsg, $bmsg, $amsg);
860             }
861              
862             # Collect any debug warnings from successful filters
863             if ($self->{DEBUG}) {
864             my @warnings = grep { $_ ne 'Admitted' } ($pmsg, $bmsg, $amsg);
865             warn "Params filter debug warnings:\n" . join("\n", @warnings) . "\n"
866             if @warnings;
867             }
868              
869             # Route each subset to appropriate handler
870             $self->add_user($person_data); # User profile
871             $self->set_subscription($biz_data); # Billing system
872             $self->set_password($auth_data); # Auth system
873              
874             # continue ...
875             B: The original C<$data> is not modified by any filter. Each call to
876             C creates its own internal copy, so the same data can be safely
877             processed by multiple filters.
878              
879             =head1 SEE ALSO
880              
881             =over 4
882              
883             =item * L - Full-featured parameter validation
884              
885             =item * L - Data structure validation
886              
887             =item * L - JSON Schema validation
888              
889             =back
890              
891             =head1 AUTHOR
892              
893             Bruce Van Allen
894              
895             =head1 LICENSE
896              
897             This module is licensed under the same terms as Perl itself.
898             See L.
899              
900             =head1 COPYRIGHT
901              
902             Copyright (C) 2026, Bruce Van Allen
903              
904             =cut
905              
906             1;