File Coverage

blib/lib/Concierge/Users/Meta.pm
Criterion Covered Total %
statement 268 337 79.5
branch 103 158 65.1
condition 38 66 57.5
subroutine 19 31 61.2
pod 13 27 48.1
total 441 619 71.2


line stmt bran cond sub pod time code
1             package Concierge::Users::Meta v0.8.0;
2 8     8   5983 use v5.36;
  8         30  
3 8     8   42 use Carp qw/ croak carp /;
  8         12  
  8         475  
4 8     8   4686 use YAML::Tiny;
  8         61209  
  8         51402  
5              
6             # ABSTRACT: Metadata for fields in Concierge::Users
7              
8             sub user_core_fields {
9 0     0 1 0 return @Concierge::Users::Meta::core_fields;
10             }
11              
12             sub user_standard_fields {
13 0     0 1 0 return @Concierge::Users::Meta::standard_fields;
14             }
15              
16             sub user_system_fields {
17 0     0 1 0 return @Concierge::Users::Meta::system_fields;
18             }
19              
20             # Get field definition for a specific field
21             # Returns complete hashref with all field attributes (type, default, validation, etc.)
22             # Returns undef if field definition not found in this instance's schema
23             sub get_field_definition {
24 353     353 1 6302 my ($self, $field) = @_;
25              
26             # Only look in instance field_definitions (set during setup)
27             # Do NOT fall back to master list - schema should be enforced
28 353 50       1259 if ($self->{field_definitions}) {
29 353         1170 return $self->{field_definitions}{$field};
30             }
31              
32             # No field_definitions available (shouldn't happen in normal use)
33 0         0 return;
34             }
35              
36              
37             # May be directly called as Concierge::Users::Meta::init_field_meta
38             # or Concierge::Users::Meta->init_field_meta
39             # Users.pm calls it with its config, which includes any
40             # field definitions from the calling app, including
41             # overrides of attributes of the standard field definitions
42             # Returns the whole backend config, which includes the field definitions
43             # as well as the ordered field list.
44             sub init_field_meta {
45 71     71 1 254063 my ($self, $config) = @_;
46 71 50       418 $config = ref $self eq __PACKAGE__ ? $config : $self;
47              
48             # Get field lists from package arrays
49 71         470 my @core_fields = @Concierge::Users::Meta::core_fields;
50 71         592 my @standard_fields = @Concierge::Users::Meta::standard_fields;
51 71         244 my @system_fields = @Concierge::Users::Meta::system_fields;
52              
53             # Assemble backend user data fields; always include core fields
54 71         242 my @fields = @core_fields;
55             # Start with built-in field definitions (clone to avoid modifying master hash)
56             my %merged_definitions = map {
57 71         213 $_ => { $Concierge::Users::Meta::field_definitions{$_}->%* }
  426         4762  
58             } @core_fields, @system_fields;
59              
60             # Add requested standard fields
61 71         251 my @included_std_fields;
62             my @requested_fields;
63 71 100 100     1009 if ( !$config->{include_standard_fields} or $config->{include_standard_fields} =~ /^all$/i ) {
64 16         79 @included_std_fields = @standard_fields;
65             }
66             else {
67 55 50       274 if ( ref $config->{include_standard_fields} eq 'ARRAY' ) {
    0          
68 55         199 @requested_fields = map { lc $_ } $config->{include_standard_fields}->@*;
  130         368  
69             }
70             elsif ( ! ref $config->{include_standard_fields} ) {
71 0         0 @requested_fields = map { lc $_ } split /\s*[,;]\s*/ => $config->{include_standard_fields};
  0         0  
72             }
73 55         134 my %standard_fields = map { $_ => 1 } @standard_fields;
  660         1476  
74 55         220 for my $fld (@requested_fields) {
75 130 50       332 if ($standard_fields{$fld}) {
76 130         370 push @included_std_fields => $fld;
77             }
78             else {
79 0         0 carp "Non-standard field requested: $fld; configure with 'app_fields => [ ...]'";
80             }
81             }
82             }
83 71         237 push @fields, @included_std_fields;
84 71         176 for my $fld (@included_std_fields) {
85             $merged_definitions{$fld} = { $Concierge::Users::Meta::field_definitions{$fld}->%* }
86 322 50       3154 if $Concierge::Users::Meta::field_definitions{$fld};
87             }
88              
89             # Process field_overrides - modify built-in field definitions
90             # Protected fields (cannot be overridden): user_id, created_date, last_mod_date
91             # Protected attributes (cannot be changed): field_name, category
92 71 100       356 if ($config->{field_overrides}) {
93             # my @protected_fields = qw/ user_id created_date last_mod_date /;
94             # my %protected_fields = map { $_ => 1 } @protected_fields;
95             # my @protected_attrs = qw/ field_name category /;
96             # my %protected_attrs = map { $_ => 1 } @protected_attrs;
97              
98             my @overrides = ref $config->{field_overrides} eq 'ARRAY'
99             ? $config->{field_overrides}->@*
100 7 50       42 : ();
101              
102 7         17 foreach my $override (@overrides) {
103 8 50       26 next unless ref $override eq 'HASH';
104              
105 8         18 my $field_name = $override->{field_name};
106 8 50       20 unless ($field_name) {
107 0         0 carp "Field override missing field_name; skipping";
108 0         0 next;
109             }
110              
111             # Check if field is protected
112             # if ($protected_fields{$field_name}) {
113 8 100       63 if ( $field_name =~ /user_id|created_date|last_mod_date/) {
114 3         836 carp "Cannot override protected field '$field_name'; skipping";
115 3         21 next;
116             }
117              
118             # Check if field exists in merged_definitions
119 5 50       16 unless ($merged_definitions{$field_name}) {
120 0         0 carp "Cannot override unknown field '$field_name'; field must be included via include_standard_fields or app_fields";
121 0         0 next;
122             }
123              
124             # Process each attribute in the override
125 5         10 my %warnings;
126 5         18 foreach my $attr (keys %$override) {
127             # Skip field_name itself (it's the identifier, not an attribute to override)
128 14 100       30 next if $attr eq 'field_name';
129              
130             # Skip protected attributes
131             # if ($protected_attrs{$attr}) {
132 9 50       25 if ($attr =~ /field_name|category/) {
133 0         0 $warnings{$attr} = "protected attribute '$attr' cannot be changed";
134 0         0 next;
135             }
136              
137             # Validate validate_as against known types
138 9 100       19 if ($attr eq 'validate_as') {
139 1         4 my $validator_type = $override->{$attr};
140             # unless ($known_validators{$validator_type}) {
141 1 50       6 unless ($Concierge::Users::Meta::type_validator_map{$validator_type}) {
142 1         17 $warnings{$attr} = "unknown validator type '$validator_type' - falling back to 'text'";
143 1         4 $merged_definitions{$field_name}{$attr} = 'text';
144 1         4 next;
145             }
146             }
147              
148             # Apply the override
149 8         16 $merged_definitions{$field_name}{$attr} = $override->{$attr};
150             }
151              
152             # Auto-update validate_as when type is changed (unless validate_as was also explicitly overridden)
153 5 100 66     21 if (exists $override->{type} && !exists $override->{validate_as}) {
154 1         2 my $new_type = $override->{type};
155             # if ($known_validators{$new_type}) {
156 1 50       4 if ($Concierge::Users::Meta::type_validator_map{$new_type}) {
157 1         2 $merged_definitions{$field_name}{validate_as} = $new_type;
158             }
159             }
160              
161             # Auto-update must_validate when required is set to 1 (unless must_validate was explicitly overridden)
162 5 50 66     24 if (exists $override->{required} && $override->{required} == 1 && !exists $override->{must_validate}) {
      66        
163 1         2 $merged_definitions{$field_name}{must_validate} = 1;
164             }
165              
166             # Emit warnings if any
167 5 100       18 if (%warnings) {
168 1         5 my $warning_list = join(', ', map { "$_: $warnings{$_}" } sort keys %warnings);
  1         7  
169 1         312 carp "Field '$field_name' override: $warning_list";
170             }
171             }
172             }
173              
174             # Add app's supplementary fields and merge their definitions
175             # But don't allow use of existing field names
176 71 100       292 if ($config->{app_fields}) {
177 12         40 my %reserved_fields = map { $_ => 1 }
  216         461  
178             @standard_fields, @core_fields, @system_fields;
179 12         40 my @app_fields;
180 12 50       61 if (ref $config->{app_fields} eq 'ARRAY' ) {
    0          
181 12         62 @app_fields = $config->{app_fields}->@*;
182             }
183             elsif (!ref $config->{app_fields} ) {
184 0         0 @app_fields = map { lc $_ } split /\s*[,;]\s*/ => $config->{app_fields};
  0         0  
185             }
186 12         43 FIELD: foreach my $field_def (@app_fields) {
187 18         43 my $field_name;
188             my $field_definition;
189 18 100       70 if (ref $field_def eq 'HASH') {
    50          
190 10         26 $field_name = $field_def->{field_name};
191 10 50       32 if ($reserved_fields{$field_name}) {
192 0         0 carp "Supplemental field name $field_name already in use";
193 0         0 next FIELD;
194             }
195             # Build complete definition for app field
196             $field_definition = {
197             field_name => $field_name,
198             label => delete $field_def->{label} || labelize($field_name),
199             category => 'app',
200             # Include all provided attributes (type, default, validation, etc.)
201             map {
202 18         80 $_ => $field_def->{$_}
203 10   66     61 } grep { !/field_name|label|category/ } keys %$field_def
  28         144  
204             };
205 10   100     74 $field_definition->{required} ||= 0;
206 10 50       31 unless ( exists $field_definition->{null_value} ) {
207 10   50     78 $field_definition->{null_value} = $Concierge::Users::Meta::type_null_values{ $field_def->{type} || 'text' };
208             }
209             } elsif ( !ref $field_def ) {
210             # Simple string field name - create minimal definition
211 8         20 $field_name = $field_def;
212 8 100       40 if ($reserved_fields{$field_name}) {
213 2         364 carp "Supplemental field name $field_name already in use";
214 2         10 next FIELD;
215             }
216             $field_definition = {
217 6         23 field_name => $field_name,
218             label => labelize($field_name),
219             category => 'app',
220             type => 'text', # Default type
221             validate_as => 'text', # Default validation
222             required => 0,
223             null_value => '',
224             };
225             }
226 16         49 $merged_definitions{$field_name} = $field_definition;
227 16         93 push @fields, $field_name;
228             }
229             }
230              
231             # Always add system fields
232 71         221 push @fields, @system_fields;
233              
234             # Auto-set defaults for enum fields that don't have explicit defaults
235             # Also create v_options (validated options) without asterisks for internal use
236 71         359 foreach my $field_name (keys %merged_definitions) {
237 764         2236 my $def = $merged_definitions{$field_name};
238              
239             # Process enum options: create v_options without asterisks
240 764 50 66     2339 if ($def->{type} eq 'enum' && $def->{options}) {
241             # Create clean options list for validation (strip leading asterisk and any following spaces)
242 178         487 my @clean_options = map { s/^\*\s*//r } $def->{options}->@*;
  913         2465  
243 178         595 $merged_definitions{$field_name}{v_options} = \@clean_options;
244              
245             # Auto-set default from * designated option for enum fields
246             # Check if default is undefined OR empty string (both should trigger auto-set)
247 178 50 33     615 if (!$def->{default} || $def->{default} eq '') {
248 178         327 my $default_option = '';
249 178         403 for my $opt ($def->{options}->@*) {
250 480 100       1293 if ($opt =~ /^\*(.+)\s*$/) {
251 145         395 $default_option = $1; # OK if it's ''
252 145         321 last;
253             }
254             }
255 178         630 $merged_definitions{$field_name}{default} = $default_option;
256             }
257             }
258             }
259              
260 71         985 my %field_meta = (
261             fields => [ @fields ],
262             field_definitions => { %merged_definitions },
263             );
264              
265 71         754 return \%field_meta;
266             }
267              
268             # Type-to-validator mapping for default validation
269             %Concierge::Users::Meta::type_validator_map = (
270             text => \&validate_text,
271             enum => \&validate_enum,
272             boolean => \&validate_boolean,
273             date => \&validate_date,
274             timestamp => \&validate_timestamp,
275             email => \&validate_email,
276             phone => \&validate_phone,
277             integer => \&validate_integer,
278             moniker => \&validate_moniker,
279             name => \&validate_name_field,
280             );
281              
282             %Concierge::Users::Meta::type_null_values = (
283             text => '',
284             enum => '',
285             boolean => '',
286             date => '0000-00-00',
287             timestamp => '0000-00-00 00:00:00',
288             email => '',
289             phone => '',
290             integer => 0,
291             moniker => '',
292             name => '',
293             );
294              
295             # Get field validator - returns validator based on validate_as or type
296             sub get_field_validator {
297 161     161 1 475 my ($self, $field) = @_;
298              
299 161         555 my $field_def = $self->get_field_definition($field);
300 161 50       509 return unless $field_def;
301              
302             # Check for validate_as specifier (JSON-serializable)
303 161 100       633 if ($field_def->{validate_as}) {
304 160         459 my $validator_type = $field_def->{validate_as};
305             return $Concierge::Users::Meta::type_validator_map{$validator_type}
306 160 50       1007 if $Concierge::Users::Meta::type_validator_map{$validator_type};
307             }
308              
309             # Return type-derived validator if available
310 1         3 my $type = $field_def->{type};
311             return $Concierge::Users::Meta::type_validator_map{$type}
312 1 50 33     12 if $type && $Concierge::Users::Meta::type_validator_map{$type};
313              
314 0         0 return; # No validator available
315             }
316              
317             # Get UI-friendly field hints for calling applications
318             # Returns hashref with: label, type, max_length, options, description, required
319             sub get_field_hints {
320 2     2 1 3164 my ($self, $field) = @_;
321              
322 2         14 my $field_def = $self->get_field_definition($field);
323 2 50       10 return unless $field_def;
324              
325             return {
326             label => $field_def->{label} || labelize($field_def->{field_name} || $field),
327             type => $field_def->{type},
328             max_length => $field_def->{max_length},
329             options => $field_def->{options},
330             description => $field_def->{description},
331             required => $field_def->{required},
332 2   33     53 };
333             }
334              
335             # Get the list of field names for this user object
336             sub get_user_fields {
337 0     0 1 0 my $self = shift;
338              
339 0         0 return $self->{fields};
340             }
341              
342             # Auto-generate label from field_name
343             sub labelize {
344 15     15 0 41 my ($field_name) = @_;
345 15 50       43 return unless $field_name;
346              
347             # Convert underscore_case to Title Case
348 15         75 $field_name =~ s/_/ /g;
349 15         189 $field_name =~ s/\b(\w)/\u$1/g;
350              
351 15         119 return $field_name;
352             }
353              
354             # Generate current date in YYYY-MM-DD format
355             sub current_date {
356 0     0 0 0 my ($mday, $mon, $year) = gmtime;
357 0         0 return sprintf("%04d-%02d-%02d", $year + 1900, $mon + 1, $mday);
358             }
359              
360             # Generate current timestamp in YYYY-MM-DD HH:MM:SS format
361             sub current_timestamp {
362 439     439 0 2442 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime;
363 439         4855 return sprintf("%04d-%02d-%02d %02d:%02d:%02d",
364             $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
365             }
366              
367             sub archive_timestamp {
368 0     0 0 0 my ($sec, $min, $hour, $mday, $mon, $year) = localtime;
369 0         0 return sprintf("%04d%02d%02d_%02d%02d%02d",
370             $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
371             }
372              
373             # ==============================================================================
374             # CONFIG DISPLAY METHODS
375             # ==============================================================================
376              
377             # Convert config hash to YAML format for storage
378             # Returns YAML string with warning header
379             sub config_to_yaml {
380 60     60 1 228 my ($config, $storage_dir) = @_;
381              
382             # Build YAML header
383 60         185 my $yaml = '';
384 60         154 $yaml .= "#" . ("#" x 78) . "\n";
385 60         155 $yaml .= "# WARNING: This is a GENERATED file for reference ONLY\n";
386 60         164 $yaml .= "#\n";
387 60         132 $yaml .= "# Editing this file will NOT affect your Users setup configuration.\n";
388 60         132 $yaml .= "#\n";
389 60         135 $yaml .= "# This file is automatically generated from:\n";
390 60         120 $yaml .= "# users-config.json\n";
391 60         112 $yaml .= "#\n";
392 60         149 $yaml .= "# This file:\n";
393 60         132 $yaml .= "# $storage_dir/users-config.yaml\n";
394 60         166 $yaml .= "#" . ("#" x 78) . "\n";
395 60         127 $yaml .= "\n";
396              
397             # Configuration metadata
398 60         112 $yaml .= "Configuration:\n";
399 60         178 $yaml .= " Version: $config->{version}\n";
400 60         205 $yaml .= " Backend: $config->{backend_module}\n";
401 60         172 $yaml .= " Storage Directory: $storage_dir\n";
402 60         155 $yaml .= " Generated: $config->{generated}\n";
403 60         153 $yaml .= "\n";
404              
405             # Field Definitions
406 60         138 $yaml .= "Field Definitions:\n";
407              
408             # Organize fields by category
409             my %fields_by_category = (
410 645         1037 'Core Fields' => [grep { my $f=$_; grep { $_ eq $f } @Concierge::Users::Meta::core_fields } @{$config->{fields}}],
  645         1121  
  2580         4808  
  60         260  
411 645         1005 'Standard Fields' => [grep { my $f=$_; grep { $_ eq $f } @Concierge::Users::Meta::standard_fields } @{$config->{fields}}],
  645         1081  
  7740         13160  
  60         231  
412 645         994 'System Fields' => [grep { my $f=$_; grep { $_ eq $f } @Concierge::Users::Meta::system_fields } @{$config->{fields}}],
  645         1011  
  1290         2365  
  60         206  
413 645         1111 'Application Fields' => [grep { my $f=$_; my $found=0;
  645         950  
414 645         1370 for my $cat (\@Concierge::Users::Meta::core_fields, \@Concierge::Users::Meta::standard_fields, \@Concierge::Users::Meta::system_fields) {
415 1935 100       3163 $found = 1 if grep { $_ eq $f } @$cat;
  11610         20832  
416             }
417 645         1782 !$found;
418 60         181 } @{$config->{fields}}],
  60         168  
419             );
420              
421 60         198 foreach my $category ('Core Fields', 'Standard Fields', 'System Fields', 'Application Fields') {
422 240         552 my $fields = $fields_by_category{$category};
423 240 100 66     1180 next unless $fields && @$fields;
424              
425 183         354 $yaml .= " $category:\n";
426 183         400 foreach my $field (@$fields) {
427 645         1365 my $def = $config->{field_definitions}{$field};
428 645 50       1267 next unless $def;
429              
430 645         1039 $yaml .= " $field:\n";
431 645         1159 $yaml .= " field_name: $def->{field_name}\n";
432 645         1203 $yaml .= " type: $def->{type}\n";
433 645         1474 $yaml .= " required: $def->{required}\n";
434              
435             # Only show validate_as if it's different from type
436 645 100 100     2221 if ($def->{validate_as} && $def->{validate_as} ne $def->{type}) {
437 133         245 $yaml .= " validate_as: $def->{validate_as}\n";
438             }
439              
440 645         1376 $yaml .= " default: " . _yaml_scalar_value($def->{default}) . "\n";
441              
442             # Show options if present
443 645 100 100     1597 if ($def->{options} && @{$def->{options}}) {
  641         2175  
444 148         276 $yaml .= " options: (asterisk '*' designates default option)\n";
445 148         227 foreach my $opt (@{$def->{options}}) {
  148         365  
446 756         1267 $yaml .= " - $opt\n";
447             }
448             }
449              
450             # Show description if present
451 645 100       1500 if ($def->{description}) {
452 640         1136 $yaml .= " description: \"$def->{description}\"\n";
453             }
454              
455             # Show other key attributes if present
456 645 100       1902 $yaml .= " max_length: $def->{max_length}\n" if $def->{max_length};
457 645 100       1465 $yaml .= " must_validate: $def->{must_validate}\n" if $def->{must_validate};
458 645         1260 $yaml .= " null_value: " . _yaml_scalar_value($def->{null_value}) . "\n";
459              
460 645         1317 $yaml .= "\n";
461             }
462             }
463              
464 60         619 return $yaml;
465             }
466              
467             # Helper to properly quote YAML scalar values
468             sub _yaml_scalar_value {
469 1290     1290   2259 my ($value) = @_;
470              
471             # Handle undefined values
472 1290 100       2557 return 'null' unless defined $value;
473              
474             # Handle empty strings
475 1286 100       2992 return '""' if $value eq '';
476              
477             # Handle numeric values
478 402 100       1654 return $value if $value =~ /^\-?\d+$/;
479              
480             # Handle boolean
481 401 50       1072 return $value if $value =~ /^[01]$/;
482              
483             # Quote strings with spaces or special chars
484 401 100       1352 return $value if $value =~ /^\S+$/;
485 266         609 return "\"$value\"";
486             }
487              
488             # Show default configuration (from __DATA__ section)
489             # Can be called as class method or instance method
490             # Parameters (optional hash):
491             # output_path => '/path/to/file.yaml' # Save to file instead of STDOUT
492             sub show_default_config {
493 0     0 1 0 my ($self, %params) = @_;
494              
495             # Read from __DATA__ section
496 0         0 my @data = ;
497 0         0 say @data;
498             }
499              
500             # Show configuration for an existing setup
501             # Must be called as instance method on a Users object
502             # Parameters (optional hash):
503             # output_path => '/path/to/file.yaml' # Save to different location
504             sub show_config {
505 0     0 1 0 my ($self, %params) = @_;
506              
507             # Check if this is a valid Users object with storage_dir
508 0 0 0     0 unless (ref $self && $self->{backend}) {
509             return {
510 0         0 success => 0,
511             message => "show_config() must be called on a Users instance. "
512             . "Use show_default_config() to view default configuration."
513             };
514             }
515              
516             # Get storage_dir from backend config
517             my $storage_dir = $self->{backend}{storage_dir}
518             or return {
519 0 0       0 success => 0,
520             message => "Cannot determine storage directory from Users object"
521             };
522              
523 0   0     0 my $yaml_file = $params{output_path} || "$storage_dir/users-config.yaml";
524              
525             # Check if YAML config file exists
526 0 0       0 unless (-f $yaml_file) {
527             return {
528 0         0 success => 0,
529             message => "Configuration file not found: $yaml_file\n"
530             . "Note: YAML config files are created automatically during setup().\n"
531             . "If you just created this setup, the file should exist. "
532             . "Otherwise, the setup may be incomplete."
533             };
534             }
535              
536             # Read and display the YAML file
537 0         0 my $yaml_content;
538 0         0 eval {
539 0 0       0 open my $fh, '<', $yaml_file or croak "Cannot open $yaml_file: $!";
540 0         0 local $/;
541 0         0 $yaml_content = <$fh>;
542 0         0 close $fh;
543             };
544 0 0       0 if ($@) {
545             return {
546 0         0 success => 0,
547             message => "Failed to read configuration file: $@"
548             };
549             }
550              
551             # Print to STDOUT
552 0         0 print $yaml_content;
553              
554             return {
555 0         0 success => 1,
556             message => "Configuration displayed from $yaml_file",
557             config_file => $yaml_file
558             };
559             }
560              
561              
562             # ==============================================================================
563             # VALIDATOR METHODS
564             # All validators receive: ($user_data, $field_name, $field_def)
565             # Validators modify $user_data->{$field_name} directly if substitution needed
566             # All validators return: { success => 1|0, message => "..." }
567             # ==============================================================================
568              
569             # Validate enum fields against options
570             sub validate_enum {
571 7     7 0 19 my ($user_data, $field_name, $field_def) = @_;
572              
573 7         17 my $value = $user_data->{$field_name};
574              
575             # Use v_options (validated options without asterisks)
576 7   50     60 my $options = $field_def->{v_options} || [];
577              
578             # Check if value is in the allowed options
579 7 100       35 if (grep { $_ eq $value } @$options) {
  27         73  
580 5         21 return { success => 1 };
581             }
582              
583             return {
584 2         21 success => 0,
585             message => "$field_def->{label} must be one of: " . join(', ', @$options),
586             };
587             }
588              
589             # Validate text fields with length checking
590             sub validate_text {
591 3     3 0 10 my ($user_data, $field_name, $field_def) = @_;
592              
593 3         10 my $value = $user_data->{$field_name};
594              
595             # Check max_length
596 3 100 66     25 if ($field_def->{max_length} && length($value) > $field_def->{max_length}) {
597             return {
598 2         19 success => 0,
599             message => "$field_def->{label} must not exceed maximum length of $field_def->{max_length} characters"
600             };
601             }
602              
603 1         5 return { success => 1 };
604             }
605              
606             # Validate email format
607             sub validate_email {
608 110     110 0 410 my ($user_data, $field_name, $field_def) = @_;
609              
610 110         330 my $value = $user_data->{$field_name};
611              
612             # Check email format
613 110 100       1195 if ($value =~ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/) {
614 108         574 return { success => 1 };
615             }
616              
617 2         8 return { success => 0, message => "$field_def->{label} must be a valid email address" };
618             }
619              
620             # Validate date format
621             sub validate_date {
622 0     0 0 0 my ($user_data, $field_name, $field_def) = @_;
623              
624 0         0 my $value = $user_data->{$field_name};
625              
626             # Check YYYY-MM-DD format
627 0 0       0 if ($value =~ /^\d{4}-\d{2}-\d{2}$/) {
628 0         0 return { success => 1 };
629             }
630              
631             return {
632 0         0 success => 0,
633             message => "Invalid date format for $field_def->{label}",
634             };
635             }
636              
637             # Validate timestamp format
638             sub validate_timestamp {
639 0     0 0 0 my ($user_data, $field_name, $field_def) = @_;
640              
641 0         0 my $value = $user_data->{$field_name};
642              
643             # Check YYYY-MM-DD HH:MM:SS format
644 0 0       0 if ($value =~ /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}$/) {
645 0         0 return { success => 1 };
646             }
647              
648             return {
649 0         0 success => 0,
650             message => "Invalid timestamp format for $field_def->{label}, using null value"
651             };
652             }
653              
654             # Validate boolean (1|0) # or resolves Perl true/false?
655             sub validate_boolean {
656 0     0 0 0 my ($user_data, $field_name, $field_def) = @_;
657              
658 0         0 my $value = $user_data->{$field_name};
659              
660             # Check if value is explicitly 0 or 1
661 0 0 0     0 if (defined $value && $value =~ /^[01]$/) {
662 0         0 return { success => 1 };
663             }
664             # Invalid boolean
665             return {
666 0         0 success => 0,
667             message => "Invalid value '$value' for boolean $field_def->{label}"
668             };
669             }
670              
671             # Validate phone format
672             sub validate_phone {
673 23     23 0 66 my ($user_data, $field_name, $field_def) = @_;
674              
675 23         63 my $value = $user_data->{$field_name};
676              
677             # Check phone format
678 23 100 66     313 if ($value =~ /^\+?[\d\s\-\(\)]+$/ && length($value) >= 7) {
679 21         91 return { success => 1 };
680             }
681              
682             # Invalid phone
683 2         14 return { success => 0, message => "$field_def->{label} must be a valid 10-character phone number" };
684             }
685              
686             # Validate integer
687             sub validate_integer {
688 0     0 0 0 my ($user_data, $field_name, $field_def) = @_;
689              
690 0         0 my $value = $user_data->{$field_name};
691              
692             # Check integer format (allow negative numbers)
693 0 0       0 if ($value =~ /^\-?\d+$/) {
694 0         0 return { success => 1 };
695             }
696              
697 0         0 return { success => 0, message => "$field_def->{label} must be a whole number" };
698             }
699              
700             sub validate_moniker {
701 2     2 0 8 my ($user_data, $field_name, $field_def) = @_;
702              
703 2         5 my $value = $user_data->{$field_name};
704              
705 2 100 66     32 return { success => 0, message => "moniker is required as 2-24 alphanumeric characters, no spaces" }
706             unless $value && $value =~ /^[a-zA-Z0-9]{2,24}$/;
707              
708 1         5 return { success => 1 };
709             }
710              
711             sub validate_name_field {
712 16     16 0 41 my ($user_data, $field_name, $field_def) = @_;
713              
714 16         38 my $value = $user_data->{$field_name};
715              
716             # Allow letters, hyphens, apostrophes, and internal spaces
717 16 100 66     137 return { success => 0, message => "$field_def->{label} contains invalid characters" }
718             unless $value
719             && $value =~ /^[a-zA-Z\u00C0-\u024F'’\-.]+(?:\s+[a-zA-Z\u00C0-\u024F'’\-.]+)*$/;
720            
721 15         49 return { success => 1 };
722             }
723              
724             @Concierge::Users::Meta::core_fields = ( qw/
725             user_id
726             moniker
727             user_status
728             access_level
729             / );
730              
731             @Concierge::Users::Meta::standard_fields = ( qw/
732             first_name
733             middle_name
734             last_name
735             prefix
736             suffix
737             organization
738             title
739             email
740             phone
741             text_ok
742             last_login_date
743             term_ends
744             / );
745              
746             @Concierge::Users::Meta::system_fields = ( qw/
747             last_mod_date
748             created_date
749             / );
750              
751             %Concierge::Users::Meta::field_definitions = (
752             # Core field definitions
753             user_id => {
754             field_name => 'user_id',
755             label => 'User ID',
756             description => 'User login ID - Primary authentication identifier',
757             type => 'system',
758             required => 1,
759             options => [],
760             default => '',
761             null_value => '',
762             max_length => 30,
763             must_validate => 0,
764             },
765             moniker => {
766             field_name => 'moniker',
767             label => 'Moniker',
768             description => 'User\'s preferred display name, nickname, or initials',
769             type => 'text',
770             required => 1,
771             options => [],
772             default => '',
773             null_value => '',
774             max_length => 24,
775             validate_as => 'moniker',
776             must_validate => 1,
777             },
778             user_status => {
779             field_name => 'user_status',
780             label => 'User Status',
781             description => 'Account status for access control',
782             type => 'enum',
783             required => 1,
784             options => ['*Eligible', 'OK', 'Inactive'],
785             default => '', # Will be auto-set to option with '*'
786             null_value => '',
787             max_length => 20,
788             validate_as => 'enum',
789             must_validate => 1,
790             },
791             access_level => {
792             field_name => 'access_level',
793             label => 'Access Level',
794             description => 'Permission level for feature access',
795             type => 'enum',
796             required => 1,
797             options => ['*anon', 'visitor', 'member', 'staff', 'admin'],
798             default => '', # Will be auto-set to option with '*'
799             null_value => '',
800             max_length => 20,
801             validate_as => 'enum',
802             must_validate => 1,
803             },
804              
805             # Standard field definitions
806             first_name => {
807             field_name => 'first_name',
808             label => 'First Name',
809             description => 'User\'s first name',
810             type => 'text',
811             required => 0,
812             options => [],
813             default => '',
814             null_value => '',
815             max_length => 50,
816             validate_as => 'name',
817             must_validate => 1,
818             },
819             middle_name => {
820             field_name => 'middle_name',
821             label => 'Middle Name',
822             description => 'User\'s middle name',
823             type => 'text',
824             required => 0,
825             options => [],
826             default => '',
827             null_value => '',
828             max_length => 50,
829             validate_as => 'name',
830             must_validate => 1,
831             },
832             last_name => {
833             field_name => 'last_name',
834             label => 'Last Name',
835             description => 'User\'s last name',
836             type => 'text',
837             required => 0,
838             options => [],
839             default => '',
840             null_value => '',
841             max_length => 50,
842             validate_as => 'name',
843             must_validate => 1,
844             },
845             prefix => {
846             field_name => 'prefix',
847             label => 'Prefix',
848             description => 'Name prefix or title',
849             type => 'enum',
850             required => 0,
851             options => ['*', 'Dr', 'Mr', 'Ms', 'Mrs', 'Mx', 'Prof', 'Hon', 'Sir', 'Madam'],
852             default => '',
853             null_value => '', # Will be auto-set to option with '*'
854             max_length => 10,
855             validate_as => 'enum',
856             must_validate => 0,
857             },
858             suffix => {
859             field_name => 'suffix',
860             label => 'Suffix',
861             description => 'Name suffix or professional designation',
862             type => 'enum',
863             required => 0,
864             options => ['*', 'Jr', 'Sr', 'II', 'III', 'IV', 'V', 'PhD', 'MD', 'DDS', 'Esq'],
865             default => '', # Will be auto-set to option with '*'
866             null_value => '',
867             max_length => 10,
868             validate_as => 'enum',
869             must_validate => 0,
870             },
871             organization => {
872             field_name => 'organization',
873             label => 'Organization',
874             description => 'User\'s organization or affiliation',
875             type => 'text',
876             required => 0,
877             options => [],
878             default => '',
879             null_value => '',
880             max_length => 100,
881             validate_as => 'text',
882             must_validate => 0,
883             },
884             title => {
885             field_name => 'title',
886             label => 'Title',
887             description => 'User\'s position or job title',
888             type => 'text',
889             required => 0,
890             options => [],
891             default => '',
892             null_value => '',
893             max_length => 100,
894             validate_as => 'text',
895             must_validate => 0,
896             },
897             email => {
898             field_name => 'email',
899             label => 'Email',
900             description => 'Email address for notifications',
901             type => 'email',
902             required => 0,
903             options => [],
904             default => '',
905             null_value => '',
906             max_length => 255,
907             validate_as => 'email',
908             must_validate => 0,
909             },
910             phone => {
911             field_name => 'phone',
912             label => 'Phone',
913             description => 'Phone number with country code',
914             type => 'phone',
915             required => 0,
916             options => [],
917             default => '',
918             null_value => '',
919             max_length => 20,
920             validate_as => 'phone',
921             must_validate => 0,
922             },
923             text_ok => {
924             field_name => 'text_ok',
925             label => 'Text OK',
926             description => 'Consent for text messages (1=yes, 0=no)',
927             type => 'boolean',
928             required => 0,
929             options => [],
930             default => '',
931             null_value => '',
932             max_length => 1,
933             validate_as => 'boolean',
934             must_validate => 0,
935             },
936             term_ends => {
937             field_name => 'term_ends',
938             label => 'Term Ends',
939             description => 'Account expiration date (YYYY-MM-DD)',
940             type => 'date',
941             required => 0,
942             options => [],
943             default => '',
944             null_value => '0000-00-00',
945             max_length => 10,
946             validate_as => 'date',
947             must_validate => 0,
948             },
949             last_login_date => {
950             field_name => 'last_login_date',
951             label => 'Last Login Date',
952             description => 'Timestamp of last successful login',
953             type => 'timestamp',
954             required => 0,
955             options => [],
956             default => '0000-00-00 00:00:00',
957             null_value => '0000-00-00 00:00:00',
958             max_length => 19,
959             validate_as => 'timestamp',
960             must_validate => 0,
961             },
962              
963             # System field definitions
964             last_mod_date => {
965             field_name => 'last_mod_date',
966             label => 'Last Modification Date',
967             description => 'Timestamp of last profile modification',
968             type => 'system',
969             required => 0,
970             options => [],
971             default => '0000-00-00 00:00:00',
972             null_value => '0000-00-00 00:00:00',
973             max_length => 19,
974             must_validate => 0,
975             },
976             created_date => {
977             field_name => 'created_date',
978             label => 'Created Date',
979             description => 'Timestamp when user account was created',
980             type => 'system',
981             required => 1,
982             options => [],
983             default => '0000-00-00 00:00:00',
984             null_value => '0000-00-00 00:00:00',
985             max_length => 19,
986             must_validate => 0,
987             },
988             );
989              
990             sub validate_user_data {
991 137     137 1 502 my ($self, $user_data) = @_;
992              
993             return { success => 1, valid_data => $user_data, message => 'Validation skipped per environment variable USERS_SKIP_VALIDATION' }
994 137 50       684 if $ENV{USERS_SKIP_VALIDATION};
995              
996 137         286 my @warnings;
997 137         441 my $validated_data = {};
998 137         780 foreach my ($field, $value) ( $user_data->%* ) {
999             # Get field definition
1000 179         957 my $field_def = $self->get_field_definition($field);
1001              
1002             # Skip unknown fields with warning
1003 179 100       589 unless (defined $field_def) {
1004 13         60 push @warnings, "Field '$field' not recognized in schema; input data skipped";
1005 13         35 next;
1006             }
1007              
1008             # Fail if a required field isn't provided a value
1009             # or the value is the same as the field's null_value
1010 166 100 66     1320 if ( !defined $value or $value eq $field_def->{null_value} ) {
1011             return { success => 0, message => "$field_def->{label} is required" }
1012 5 50       24 if $field_def->{required}; # Stops input
1013 5         18 next; # OK if value is null_value and not required,
1014             # but no need to validate or input
1015             }
1016              
1017             # Get validator for this field
1018 161         810 my $validator = $self->get_field_validator($field);
1019 161 50       517 unless ($validator) { # No validator available, skip
1020 0         0 push @warnings => "Validator not found for '$field'; input skipped";
1021 0         0 next;
1022             }
1023              
1024             # Run validator
1025 161         661 my $result = $validator->($user_data, $field, $field_def);
1026              
1027             # Collect warnings
1028 161 100       581 if ($result->{message}) {
1029 10         37 push @warnings, "$field: $result->{message}";
1030             }
1031              
1032             # Only validated data will be returned
1033 161 100       508 if ($result->{success}) {
    100          
1034 151         866 $validated_data->{$field} = $value;
1035             }
1036             # Fail on validation errors only if must_validate is set for a field
1037             elsif ($field_def->{must_validate}) {
1038 4         34 return { success => 0, message => $result->{message}, field => $field };
1039             }
1040             }
1041              
1042             # Return success with validated data and any warnings
1043 133         559 my $outcome = { success => 1, valid_data => $validated_data };
1044 133 100       456 $outcome->{warnings} = \@warnings if @warnings;
1045 133         507 return $outcome;
1046             }
1047              
1048             # Parse filter DSL string into filter structure
1049             sub parse_filter_string {
1050 5     5 1 16 my ($self, $filter_string) = @_;
1051              
1052 5         40 my @or_groups = split /\s*\|\s*/, $filter_string;
1053 5         13 my @parsed_filters;
1054              
1055 5         14 foreach my $group (@or_groups) {
1056 5         15 my @and_conditions = split /\s*;\s*/, $group;
1057 5         10 my @parsed_and;
1058              
1059 5         12 foreach my $condition (@and_conditions) {
1060             # Parse [field][op][value]
1061 5 50       41 if ($condition =~ /^(\w+)(=|:|!|>|<)(.+)$/) {
1062 5         32 my ($field, $op, $value) = ($1, $2, $3);
1063              
1064             # Validate field exists
1065 5 50       11 unless (grep { $_ eq $field } @{$self->{fields}}) {
  44         100  
  5         19  
1066 0         0 carp "Warning: Unknown field '$field' in filter";
1067 0         0 next;
1068             }
1069              
1070 5         59 push @parsed_and, {
1071             field => $field,
1072             op => $op,
1073             value => $value
1074             };
1075             } else {
1076 0         0 carp "Warning: Invalid filter condition '$condition'";
1077             }
1078             }
1079              
1080             # Only add non-empty AND groups
1081 5 50       20 if (@parsed_and) {
1082 5         20 push @parsed_filters, \@parsed_and;
1083             }
1084             }
1085              
1086             # Return empty hash if no valid filters
1087 5 50       18 return {} unless @parsed_filters;
1088              
1089             # Return structure for backend processing
1090             return {
1091 5         31 or_groups => \@parsed_filters,
1092             raw => $filter_string
1093             };
1094             }
1095              
1096             1;
1097              
1098             =head1 NAME
1099              
1100             Concierge::Users::Meta - Field definitions, validators, and configuration
1101             utilities for Concierge::Users
1102              
1103             =head1 VERSION
1104              
1105             v0.7.4
1106              
1107             =head1 SYNOPSIS
1108              
1109             use Concierge::Users;
1110              
1111             my $users = Concierge::Users->new('/path/to/users-config.json');
1112              
1113             # Introspect field schema
1114             my $fields = $users->get_user_fields(); # ordered field list
1115             my $def = $users->get_field_definition('email');
1116             my $hints = $users->get_field_hints('email');
1117              
1118             # Class-level field lists
1119             my @core = Concierge::Users::Meta::user_core_fields();
1120             my @std = Concierge::Users::Meta::user_standard_fields();
1121             my @sys = Concierge::Users::Meta::user_system_fields();
1122              
1123             # Display configuration
1124             Concierge::Users::Meta->show_default_config(); # built-in defaults
1125             $users->show_config(); # active setup
1126              
1127             =head1 DESCRIPTION
1128              
1129             Concierge::Users::Meta is the parent class for L and all
1130             storage backends. It owns the master field definitions, the validation
1131             subsystem, the filter DSL parser, and the configuration display helpers.
1132             Application code normally interacts with Meta indirectly through a
1133             L instance, but the introspection methods and class-level
1134             field lists are available for direct use.
1135              
1136             =head1 FIELD CATALOG
1137              
1138             Every user record is composed of fields drawn from three built-in
1139             categories plus an optional application category.
1140              
1141             =head2 Core Fields (4)
1142              
1143             Always present in every setup.
1144              
1145             =over 4
1146              
1147             =item B
1148              
1149             Primary authentication identifier.
1150              
1151             type: system
1152             required: 1
1153             max_length: 30
1154             default: ""
1155             null_value: ""
1156             must_validate: 0
1157             description: User login ID - Primary authentication identifier
1158              
1159             =item B
1160              
1161             User's preferred display name, nickname, or initials.
1162              
1163             type: text
1164             validate_as: moniker
1165             required: 1
1166             max_length: 24
1167             default: ""
1168             null_value: ""
1169             must_validate: 1
1170             description: User's preferred display name, nickname, or initials
1171              
1172             =item B
1173              
1174             Account status for access control.
1175              
1176             type: enum
1177             validate_as: enum
1178             required: 1
1179             options: *Eligible, OK, Inactive
1180             max_length: 20
1181             default: Eligible (auto-set from * option)
1182             null_value: ""
1183             must_validate: 1
1184              
1185             This is a core field (always present), but its C can be
1186             replaced via C to match your application's workflow.
1187             See L for an example.
1188              
1189             =item B
1190              
1191             Permission level for feature access.
1192              
1193             type: enum
1194             validate_as: enum
1195             required: 1
1196             options: *anon, visitor, member, staff, admin
1197             max_length: 20
1198             default: anon (auto-set from * option)
1199             null_value: ""
1200             must_validate: 1
1201              
1202             Core field (always present); C can be replaced via
1203             C. See L.
1204              
1205             =back
1206              
1207             =head2 Standard Fields (12)
1208              
1209             Included by default when C is omitted or set
1210             to C<'all'>. Pass an arrayref of names to select specific fields, or
1211             an empty arrayref C<[]> to exclude all standard fields.
1212              
1213             B
1214              
1215             =over 4
1216              
1217             =item B -- type C, validate_as C, max 50, must_validate 1
1218              
1219             =item B -- type C, validate_as C, max 50, must_validate 1
1220              
1221             =item B -- type C, validate_as C, max 50, must_validate 1
1222              
1223             =item B -- type C, options: (none) Dr Mr Ms Mrs Mx Prof Hon Sir Madam, max 10
1224              
1225             =item B -- type C, options: (none) Jr Sr II III IV V PhD MD DDS Esq, max 10
1226              
1227             =back
1228              
1229             B
1230              
1231             =over 4
1232              
1233             =item B -- type C, validate_as C, max 100
1234              
1235             =item B -- type C<text>, validate_as C<text>, max 100 </td> </tr> <tr> <td class="h" > <a name="1236">1236</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1237">1237</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1238">1238</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1239">1239</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> B<Contact fields:> </td> </tr> <tr> <td class="h" > <a name="1240">1240</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1241">1241</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1242">1242</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1243">1243</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<email> -- type C<email>, validate_as C<email>, max 255 </td> </tr> <tr> <td class="h" > <a name="1244">1244</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1245">1245</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<phone> -- type C<phone>, validate_as C<phone>, max 20 </td> </tr> <tr> <td class="h" > <a name="1246">1246</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1247">1247</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<text_ok> -- type C<boolean>, validate_as C<boolean>, null_value "", max 1 </td> </tr> <tr> <td class="h" > <a name="1248">1248</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1249">1249</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1250">1250</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1251">1251</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> B<Temporal fields:> </td> </tr> <tr> <td class="h" > <a name="1252">1252</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1253">1253</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1254">1254</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1255">1255</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<last_login_date> -- type C<timestamp>, validate_as C<timestamp>, </td> </tr> <tr> <td class="h" > <a name="1256">1256</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> default C<0000-00-00 00:00:00>, max 19 </td> </tr> <tr> <td class="h" > <a name="1257">1257</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1258">1258</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<term_ends> -- type C<date>, validate_as C<date>, </td> </tr> <tr> <td class="h" > <a name="1259">1259</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value C<0000-00-00>, max 10 </td> </tr> <tr> <td class="h" > <a name="1260">1260</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1261">1261</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1262">1262</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1263">1263</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> All standard fields have C<required =E<gt> 0> by default. </td> </tr> <tr> <td class="h" > <a name="1264">1264</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1265">1265</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 System Fields (2) </td> </tr> <tr> <td class="h" > <a name="1266">1266</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1267">1267</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Always appended to the field list. Auto-managed by the backends; </td> </tr> <tr> <td class="h" > <a name="1268">1268</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> cannot be set through the public API. Protected from overrides. </td> </tr> <tr> <td class="h" > <a name="1269">1269</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1270">1270</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1271">1271</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1272">1272</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<last_mod_date> -- type C<system>, timestamp updated on every write </td> </tr> <tr> <td class="h" > <a name="1273">1273</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1274">1274</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<created_date> -- type C<system>, timestamp set once on creation, </td> </tr> <tr> <td class="h" > <a name="1275">1275</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<required =E<gt> 1> </td> </tr> <tr> <td class="h" > <a name="1276">1276</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1277">1277</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1278">1278</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1279">1279</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 FIELD ATTRIBUTES </td> </tr> <tr> <td class="h" > <a name="1280">1280</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1281">1281</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Each field definition is a hashref that may contain the following keys: </td> </tr> <tr> <td class="h" > <a name="1282">1282</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1283">1283</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1284">1284</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1285">1285</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<field_name> -- Internal name (snake_case). Used as hash key and </td> </tr> <tr> <td class="h" > <a name="1286">1286</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> column/file identifier. </td> </tr> <tr> <td class="h" > <a name="1287">1287</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1288">1288</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<category> -- One of C<core>, C<standard>, C<system>, or C<app>. </td> </tr> <tr> <td class="h" > <a name="1289">1289</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Set automatically; protected from overrides. </td> </tr> <tr> <td class="h" > <a name="1290">1290</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1291">1291</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<type> -- Data type: C<text>, C<email>, C<phone>, C<date>, </td> </tr> <tr> <td class="h" > <a name="1292">1292</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<timestamp>, C<boolean>, C<integer>, C<enum>, C<system>. </td> </tr> <tr> <td class="h" > <a name="1293">1293</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1294">1294</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<validate_as> -- Validator to use if different from C<type>. </td> </tr> <tr> <td class="h" > <a name="1295">1295</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> See L</VALIDATOR TYPES>. </td> </tr> <tr> <td class="h" > <a name="1296">1296</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1297">1297</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<label> -- Human-readable label for UI display. Auto-generated </td> </tr> <tr> <td class="h" > <a name="1298">1298</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> from C<field_name> if omitted. </td> </tr> <tr> <td class="h" > <a name="1299">1299</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1300">1300</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<description> -- Short explanatory text for documentation or UI </td> </tr> <tr> <td class="h" > <a name="1301">1301</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> hints. </td> </tr> <tr> <td class="h" > <a name="1302">1302</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1303">1303</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<required> -- C<1> if the field must have a non-null value on </td> </tr> <tr> <td class="h" > <a name="1304">1304</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> creation; C<0> otherwise. </td> </tr> <tr> <td class="h" > <a name="1305">1305</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1306">1306</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<must_validate> -- C<1> if a validation failure should reject the </td> </tr> <tr> <td class="h" > <a name="1307">1307</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> entire operation; C<0> to treat failure as a non-fatal warning. </td> </tr> <tr> <td class="h" > <a name="1308">1308</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1309">1309</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<options> -- Arrayref of allowed values for C<enum> fields. </td> </tr> <tr> <td class="h" > <a name="1310">1310</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Prefix one option with C<*> to designate the default (see </td> </tr> <tr> <td class="h" > <a name="1311">1311</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L</Enum Default Convention>). </td> </tr> <tr> <td class="h" > <a name="1312">1312</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1313">1313</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<default> -- Value assigned to the field on new-record creation </td> </tr> <tr> <td class="h" > <a name="1314">1314</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> when no value is supplied. </td> </tr> <tr> <td class="h" > <a name="1315">1315</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1316">1316</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<null_value> -- Sentinel that represents "no data" for this field </td> </tr> <tr> <td class="h" > <a name="1317">1317</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> type (e.g. C<""> for text, C<""> for boolean, C<0000-00-00> for date). </td> </tr> <tr> <td class="h" > <a name="1318">1318</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1319">1319</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<max_length> -- Maximum character length enforced by the C<text> </td> </tr> <tr> <td class="h" > <a name="1320">1320</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> validator and used as a UI hint. </td> </tr> <tr> <td class="h" > <a name="1321">1321</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1322">1322</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1323">1323</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1324">1324</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 VALIDATOR TYPES </td> </tr> <tr> <td class="h" > <a name="1325">1325</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1326">1326</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Ten built-in validators are available. Each is selected by the field's </td> </tr> <tr> <td class="h" > <a name="1327">1327</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<validate_as> (or C<type> as fallback) and receives </td> </tr> <tr> <td class="h" > <a name="1328">1328</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<($user_data, $field_name, $field_def)>. </td> </tr> <tr> <td class="h" > <a name="1329">1329</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1330">1330</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1331">1331</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1332">1332</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<text> </td> </tr> <tr> <td class="h" > <a name="1333">1333</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1334">1334</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Validates C<max_length> if defined. Accepts any string. </td> </tr> <tr> <td class="h" > <a name="1335">1335</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1336">1336</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1337">1337</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1338">1338</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<email> </td> </tr> <tr> <td class="h" > <a name="1339">1339</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1340">1340</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Pattern: C<< /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ >> </td> </tr> <tr> <td class="h" > <a name="1341">1341</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1342">1342</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1343">1343</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1344">1344</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<phone> </td> </tr> <tr> <td class="h" > <a name="1345">1345</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1346">1346</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Digits, spaces, hyphens, parentheses, optional leading C<+>; </td> </tr> <tr> <td class="h" > <a name="1347">1347</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> minimum 7 characters. </td> </tr> <tr> <td class="h" > <a name="1348">1348</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1349">1349</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1350">1350</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1351">1351</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<date> </td> </tr> <tr> <td class="h" > <a name="1352">1352</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1353">1353</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Pattern: C<YYYY-MM-DD> (C<< /^\d{4}-\d{2}-\d{2}$/ >>). </td> </tr> <tr> <td class="h" > <a name="1354">1354</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1355">1355</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "0000-00-00" </td> </tr> <tr> <td class="h" > <a name="1356">1356</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1357">1357</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<timestamp> </td> </tr> <tr> <td class="h" > <a name="1358">1358</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1359">1359</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Pattern: C<YYYY-MM-DD HH:MM:SS> or C<YYYY-MM-DDTHH:MM:SS>. </td> </tr> <tr> <td class="h" > <a name="1360">1360</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1361">1361</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "0000-00-00 00:00:00" </td> </tr> <tr> <td class="h" > <a name="1362">1362</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1363">1363</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<boolean> </td> </tr> <tr> <td class="h" > <a name="1364">1364</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1365">1365</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Strictly C<0> or C<1>. </td> </tr> <tr> <td class="h" > <a name="1366">1366</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1367">1367</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1368">1368</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1369">1369</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<integer> </td> </tr> <tr> <td class="h" > <a name="1370">1370</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1371">1371</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Optional leading minus, digits only (C<< /^\-?\d+$/ >>). </td> </tr> <tr> <td class="h" > <a name="1372">1372</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1373">1373</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1374">1374</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1375">1375</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<enum> </td> </tr> <tr> <td class="h" > <a name="1376">1376</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1377">1377</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Value must appear in the field's C<v_options> list (options with C<*> </td> </tr> <tr> <td class="h" > <a name="1378">1378</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> prefix stripped). </td> </tr> <tr> <td class="h" > <a name="1379">1379</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1380">1380</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1381">1381</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1382">1382</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<moniker> </td> </tr> <tr> <td class="h" > <a name="1383">1383</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1384">1384</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> 2-24 alphanumeric characters, no spaces </td> </tr> <tr> <td class="h" > <a name="1385">1385</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> (C<< /^[a-zA-Z0-9]{2,24}$/ >>). </td> </tr> <tr> <td class="h" > <a name="1386">1386</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1387">1387</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1388">1388</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1389">1389</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<name> </td> </tr> <tr> <td class="h" > <a name="1390">1390</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1391">1391</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Letters (including accented), hyphens, apostrophes, and internal </td> </tr> <tr> <td class="h" > <a name="1392">1392</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> spaces. </td> </tr> <tr> <td class="h" > <a name="1393">1393</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1394">1394</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> null_value: "" </td> </tr> <tr> <td class="h" > <a name="1395">1395</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1396">1396</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1397">1397</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1398">1398</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 validate_as vs type </td> </tr> <tr> <td class="h" > <a name="1399">1399</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1400">1400</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> A field's C<type> declares its data type and determines the default </td> </tr> <tr> <td class="h" > <a name="1401">1401</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> validator. C<validate_as> overrides the validator without changing </td> </tr> <tr> <td class="h" > <a name="1402">1402</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the type. For example, an application field with C<< type => 'text' >> </td> </tr> <tr> <td class="h" > <a name="1403">1403</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> and C<< validate_as => 'moniker' >> is stored as text but validated with </td> </tr> <tr> <td class="h" > <a name="1404">1404</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the moniker pattern. When C<type> is changed via a field override and </td> </tr> <tr> <td class="h" > <a name="1405">1405</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<validate_as> is not explicitly set, C<validate_as> is updated </td> </tr> <tr> <td class="h" > <a name="1406">1406</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> automatically to match the new type. </td> </tr> <tr> <td class="h" > <a name="1407">1407</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1408">1408</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 must_validate Behavior </td> </tr> <tr> <td class="h" > <a name="1409">1409</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1410">1410</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> When C<must_validate> is C<1> for a field, a validation failure causes </td> </tr> <tr> <td class="h" > <a name="1411">1411</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the entire C<register_user> or C<update_user> call to return </td> </tr> <tr> <td class="h" > <a name="1412">1412</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<< { success => 0 } >>. When C<must_validate> is C<0>, the field's </td> </tr> <tr> <td class="h" > <a name="1413">1413</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> value is silently dropped and a warning is appended to the response. </td> </tr> <tr> <td class="h" > <a name="1414">1414</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1415">1415</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Setting C<< required => 1 >> in a field override automatically enables </td> </tr> <tr> <td class="h" > <a name="1416">1416</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<must_validate> unless C<must_validate> is explicitly set in the same </td> </tr> <tr> <td class="h" > <a name="1417">1417</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> override. </td> </tr> <tr> <td class="h" > <a name="1418">1418</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1419">1419</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The environment variable C<USERS_SKIP_VALIDATION> bypasses all </td> </tr> <tr> <td class="h" > <a name="1420">1420</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> validation when set to a true value. </td> </tr> <tr> <td class="h" > <a name="1421">1421</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1422">1422</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 FIELD CUSTOMIZATION </td> </tr> <tr> <td class="h" > <a name="1423">1423</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1424">1424</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Application Fields </td> </tr> <tr> <td class="h" > <a name="1425">1425</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1426">1426</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Pass C<app_fields> to C<< Concierge::Users->setup() >> as an arrayref. </td> </tr> <tr> <td class="h" > <a name="1427">1427</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Each element is either a string (minimal definition) or a hashref (full </td> </tr> <tr> <td class="h" > <a name="1428">1428</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> definition): </td> </tr> <tr> <td class="h" > <a name="1429">1429</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1430">1430</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> app_fields => [ </td> </tr> <tr> <td class="h" > <a name="1431">1431</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> 'nickname', # string shorthand </td> </tr> <tr> <td class="h" > <a name="1432">1432</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> { # full definition </td> </tr> <tr> <td class="h" > <a name="1433">1433</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> field_name => 'department', </td> </tr> <tr> <td class="h" > <a name="1434">1434</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> type => 'enum', </td> </tr> <tr> <td class="h" > <a name="1435">1435</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> options => ['*Engineering', 'Sales', 'Support'], </td> </tr> <tr> <td class="h" > <a name="1436">1436</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> required => 1, </td> </tr> <tr> <td class="h" > <a name="1437">1437</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> label => 'Department', </td> </tr> <tr> <td class="h" > <a name="1438">1438</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }, </td> </tr> <tr> <td class="h" > <a name="1439">1439</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ], </td> </tr> <tr> <td class="h" > <a name="1440">1440</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1441">1441</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> String shorthand creates a field with C<< type => 'text' >>, </td> </tr> <tr> <td class="h" > <a name="1442">1442</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<< validate_as => 'text' >>, C<< required => 0 >>. </td> </tr> <tr> <td class="h" > <a name="1443">1443</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1444">1444</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Reserved names (any core, standard, or system field name) are rejected </td> </tr> <tr> <td class="h" > <a name="1445">1445</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> with a warning. </td> </tr> <tr> <td class="h" > <a name="1446">1446</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1447">1447</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Field Overrides </td> </tr> <tr> <td class="h" > <a name="1448">1448</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1449">1449</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Pass C<field_overrides> to C<setup()> as an arrayref of hashrefs. </td> </tr> <tr> <td class="h" > <a name="1450">1450</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Each must contain C<field_name> to identify the target: </td> </tr> <tr> <td class="h" > <a name="1451">1451</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1452">1452</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> field_overrides => [ </td> </tr> <tr> <td class="h" > <a name="1453">1453</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> { </td> </tr> <tr> <td class="h" > <a name="1454">1454</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> field_name => 'email', </td> </tr> <tr> <td class="h" > <a name="1455">1455</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> required => 1, </td> </tr> <tr> <td class="h" > <a name="1456">1456</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> label => 'Work Email', </td> </tr> <tr> <td class="h" > <a name="1457">1457</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }, </td> </tr> <tr> <td class="h" > <a name="1458">1458</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ], </td> </tr> <tr> <td class="h" > <a name="1459">1459</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1460">1460</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> B<Protected fields> that cannot be overridden: C<user_id>, </td> </tr> <tr> <td class="h" > <a name="1461">1461</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<created_date>, C<last_mod_date>. </td> </tr> <tr> <td class="h" > <a name="1462">1462</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1463">1463</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> B<Protected attributes> that cannot be changed: C<field_name>, </td> </tr> <tr> <td class="h" > <a name="1464">1464</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<category>. </td> </tr> <tr> <td class="h" > <a name="1465">1465</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1466">1466</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Auto-behaviors: </td> </tr> <tr> <td class="h" > <a name="1467">1467</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1468">1468</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over 4 </td> </tr> <tr> <td class="h" > <a name="1469">1469</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1470">1470</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * Changing C<type> auto-updates C<validate_as> to match (unless </td> </tr> <tr> <td class="h" > <a name="1471">1471</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<validate_as> is also specified). </td> </tr> <tr> <td class="h" > <a name="1472">1472</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1473">1473</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * Setting C<< required => 1 >> auto-enables C<must_validate> </td> </tr> <tr> <td class="h" > <a name="1474">1474</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> (unless C<must_validate> is also specified). </td> </tr> <tr> <td class="h" > <a name="1475">1475</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1476">1476</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * An unknown C<validate_as> value falls back to C<text> with a </td> </tr> <tr> <td class="h" > <a name="1477">1477</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> warning. </td> </tr> <tr> <td class="h" > <a name="1478">1478</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1479">1479</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="1480">1480</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1481">1481</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> B<Overriding enum options:> Core fields like C<user_status> and </td> </tr> <tr> <td class="h" > <a name="1482">1482</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<access_level> are always present, but their C<options> are not </td> </tr> <tr> <td class="h" > <a name="1483">1483</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> fixed. Replace them with values that fit your domain: </td> </tr> <tr> <td class="h" > <a name="1484">1484</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1485">1485</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Makerspace member status instead of the default </td> </tr> <tr> <td class="h" > <a name="1486">1486</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Eligible / OK / Inactive </td> </tr> <tr> <td class="h" > <a name="1487">1487</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> field_overrides => [ </td> </tr> <tr> <td class="h" > <a name="1488">1488</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> { </td> </tr> <tr> <td class="h" > <a name="1489">1489</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> field_name => 'user_status', </td> </tr> <tr> <td class="h" > <a name="1490">1490</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> options => [qw( *Applicant Novice Skilled </td> </tr> <tr> <td class="h" > <a name="1491">1491</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Expert Mentor Steward )], </td> </tr> <tr> <td class="h" > <a name="1492">1492</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }, </td> </tr> <tr> <td class="h" > <a name="1493">1493</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ], </td> </tr> <tr> <td class="h" > <a name="1494">1494</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1495">1495</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The C<*>-prefixed option becomes the default (see L</Enum Default </td> </tr> <tr> <td class="h" > <a name="1496">1496</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Convention>). Validation, filtering, and all other enum behaviors </td> </tr> <tr> <td class="h" > <a name="1497">1497</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> apply to the new option set automatically. </td> </tr> <tr> <td class="h" > <a name="1498">1498</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1499">1499</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Enum Default Convention </td> </tr> <tr> <td class="h" > <a name="1500">1500</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1501">1501</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> In an C<options> arrayref, prefix exactly one value with C<*> to mark it </td> </tr> <tr> <td class="h" > <a name="1502">1502</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> as the default: </td> </tr> <tr> <td class="h" > <a name="1503">1503</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1504">1504</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> options => ['*Free', 'Premium', 'Enterprise'] </td> </tr> <tr> <td class="h" > <a name="1505">1505</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1506">1506</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The C<*> is stripped for validation (stored internally in C<v_options>). </td> </tr> <tr> <td class="h" > <a name="1507">1507</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> If no explicit C<default> is set for the field, the C<*>-marked option </td> </tr> <tr> <td class="h" > <a name="1508">1508</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> becomes the default automatically. A bare C<*> (e.g. in C<prefix> and </td> </tr> <tr> <td class="h" > <a name="1509">1509</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<suffix>) represents an empty default. </td> </tr> <tr> <td class="h" > <a name="1510">1510</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1511">1511</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 FILTER DSL </td> </tr> <tr> <td class="h" > <a name="1512">1512</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1513">1513</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The C<list_users> method accepts a filter string with five operators and </td> </tr> <tr> <td class="h" > <a name="1514">1514</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> two combinators. </td> </tr> <tr> <td class="h" > <a name="1515">1515</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1516">1516</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Operators </td> </tr> <tr> <td class="h" > <a name="1517">1517</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1518">1518</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> = exact match user_status=OK </td> </tr> <tr> <td class="h" > <a name="1519">1519</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> : substring (case-insensitive) last_name:smith </td> </tr> <tr> <td class="h" > <a name="1520">1520</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ! not-contains (case-insensitive) email!example.org </td> </tr> <tr> <td class="h" > <a name="1521">1521</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> > greater than (string) last_login_date>2025-01-01 </td> </tr> <tr> <td class="h" > <a name="1522">1522</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> < less than (string) term_ends<2026-01-01 </td> </tr> <tr> <td class="h" > <a name="1523">1523</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1524">1524</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Combinators </td> </tr> <tr> <td class="h" > <a name="1525">1525</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1526">1526</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ; AND -- all conditions must match </td> </tr> <tr> <td class="h" > <a name="1527">1527</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> | OR -- at least one group must match </td> </tr> <tr> <td class="h" > <a name="1528">1528</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1529">1529</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> AND binds tighter than OR: C<a=1;b=2|c=3> means </td> </tr> <tr> <td class="h" > <a name="1530">1530</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<(a=1 AND b=2) OR (c=3)>. </td> </tr> <tr> <td class="h" > <a name="1531">1531</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1532">1532</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Examples </td> </tr> <tr> <td class="h" > <a name="1533">1533</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1534">1534</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Active members </td> </tr> <tr> <td class="h" > <a name="1535">1535</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> user_status=OK;access_level=member </td> </tr> <tr> <td class="h" > <a name="1536">1536</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1537">1537</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Staff or admin </td> </tr> <tr> <td class="h" > <a name="1538">1538</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> access_level=staff|access_level=admin </td> </tr> <tr> <td class="h" > <a name="1539">1539</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1540">1540</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Name search with status filter </td> </tr> <tr> <td class="h" > <a name="1541">1541</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> last_name:Garcia;user_status=OK </td> </tr> <tr> <td class="h" > <a name="1542">1542</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1543">1543</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> # Recent logins </td> </tr> <tr> <td class="h" > <a name="1544">1544</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> last_login_date>2025-06-01 </td> </tr> <tr> <td class="h" > <a name="1545">1545</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1546">1546</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Unknown fields in a filter string produce a warning and are skipped. </td> </tr> <tr> <td class="h" > <a name="1547">1547</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1548">1548</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 METHODS </td> </tr> <tr> <td class="h" > <a name="1549">1549</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1550">1550</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Class Methods </td> </tr> <tr> <td class="h" > <a name="1551">1551</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1552">1552</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 user_core_fields </td> </tr> <tr> <td class="h" > <a name="1553">1553</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1554">1554</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my @fields = Concierge::Users::Meta::user_core_fields(); </td> </tr> <tr> <td class="h" > <a name="1555">1555</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1556">1556</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the list of core field names: </td> </tr> <tr> <td class="h" > <a name="1557">1557</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<user_id>, C<moniker>, C<user_status>, C<access_level>. </td> </tr> <tr> <td class="h" > <a name="1558">1558</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1559">1559</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 user_standard_fields </td> </tr> <tr> <td class="h" > <a name="1560">1560</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1561">1561</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my @fields = Concierge::Users::Meta::user_standard_fields(); </td> </tr> <tr> <td class="h" > <a name="1562">1562</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1563">1563</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the list of standard field names (12 fields). </td> </tr> <tr> <td class="h" > <a name="1564">1564</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1565">1565</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 user_system_fields </td> </tr> <tr> <td class="h" > <a name="1566">1566</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1567">1567</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my @fields = Concierge::Users::Meta::user_system_fields(); </td> </tr> <tr> <td class="h" > <a name="1568">1568</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1569">1569</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the list of system field names: C<last_mod_date>, </td> </tr> <tr> <td class="h" > <a name="1570">1570</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<created_date>. </td> </tr> <tr> <td class="h" > <a name="1571">1571</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1572">1572</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 init_field_meta </td> </tr> <tr> <td class="h" > <a name="1573">1573</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1574">1574</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $meta = Concierge::Users::Meta::init_field_meta(\%config); </td> </tr> <tr> <td class="h" > <a name="1575">1575</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1576">1576</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Processes the setup configuration and returns a hashref with C<fields> </td> </tr> <tr> <td class="h" > <a name="1577">1577</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> (ordered arrayref) and C<field_definitions> (hashref of field </td> </tr> <tr> <td class="h" > <a name="1578">1578</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> definitions). Called internally by C<< Concierge::Users->setup() >>. </td> </tr> <tr> <td class="h" > <a name="1579">1579</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1580">1580</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 show_default_config </td> </tr> <tr> <td class="h" > <a name="1581">1581</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1582">1582</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Concierge::Users::Meta->show_default_config(); </td> </tr> <tr> <td class="h" > <a name="1583">1583</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1584">1584</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Prints the built-in default field configuration template to STDOUT. </td> </tr> <tr> <td class="h" > <a name="1585">1585</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1586">1586</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 Instance Methods </td> </tr> <tr> <td class="h" > <a name="1587">1587</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1588">1588</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 get_field_definition </td> </tr> <tr> <td class="h" > <a name="1589">1589</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1590">1590</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $def = $users->get_field_definition('email'); </td> </tr> <tr> <td class="h" > <a name="1591">1591</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1592">1592</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the complete field definition hashref for the named field, or </td> </tr> <tr> <td class="h" > <a name="1593">1593</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<undef> if the field is not in the current schema. </td> </tr> <tr> <td class="h" > <a name="1594">1594</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1595">1595</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 get_field_validator </td> </tr> <tr> <td class="h" > <a name="1596">1596</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1597">1597</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $code_ref = $users->get_field_validator('email'); </td> </tr> <tr> <td class="h" > <a name="1598">1598</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1599">1599</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the validator code reference for the named field based on its </td> </tr> <tr> <td class="h" > <a name="1600">1600</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<validate_as> or C<type>, or C<undef> if no validator is available. </td> </tr> <tr> <td class="h" > <a name="1601">1601</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1602">1602</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 get_field_hints </td> </tr> <tr> <td class="h" > <a name="1603">1603</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1604">1604</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $hints = $users->get_field_hints('email'); </td> </tr> <tr> <td class="h" > <a name="1605">1605</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1606">1606</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns a hashref of UI-friendly attributes: C<label>, C<type>, </td> </tr> <tr> <td class="h" > <a name="1607">1607</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<max_length>, C<options>, C<description>, C<required>. </td> </tr> <tr> <td class="h" > <a name="1608">1608</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1609">1609</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 get_user_fields </td> </tr> <tr> <td class="h" > <a name="1610">1610</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1611">1611</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $fields = $users->get_user_fields(); </td> </tr> <tr> <td class="h" > <a name="1612">1612</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1613">1613</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the ordered arrayref of field names for this instance's schema. </td> </tr> <tr> <td class="h" > <a name="1614">1614</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1615">1615</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 validate_user_data </td> </tr> <tr> <td class="h" > <a name="1616">1616</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1617">1617</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $result = $users->validate_user_data(\%data); </td> </tr> <tr> <td class="h" > <a name="1618">1618</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1619">1619</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Validates C<%data> against the field schema. Returns </td> </tr> <tr> <td class="h" > <a name="1620">1620</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<< { success => 1, valid_data => \%clean } >> on success (with optional </td> </tr> <tr> <td class="h" > <a name="1621">1621</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<warnings> arrayref), or C<< { success => 0, message => $reason } >> </td> </tr> <tr> <td class="h" > <a name="1622">1622</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> on failure. </td> </tr> <tr> <td class="h" > <a name="1623">1623</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1624">1624</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 parse_filter_string </td> </tr> <tr> <td class="h" > <a name="1625">1625</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1626">1626</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $filters = $users->parse_filter_string('user_status=OK;access_level=member'); </td> </tr> <tr> <td class="h" > <a name="1627">1627</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1628">1628</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Parses a filter DSL string into an internal structure suitable for </td> </tr> <tr> <td class="h" > <a name="1629">1629</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> backend list methods. See L</FILTER DSL>. </td> </tr> <tr> <td class="h" > <a name="1630">1630</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1631">1631</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 show_config </td> </tr> <tr> <td class="h" > <a name="1632">1632</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1633">1633</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $users->show_config(); </td> </tr> <tr> <td class="h" > <a name="1634">1634</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $users->show_config(output_path => '/tmp/config.yaml'); </td> </tr> <tr> <td class="h" > <a name="1635">1635</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1636">1636</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Prints the active YAML configuration file for this instance to STDOUT. </td> </tr> <tr> <td class="h" > <a name="1637">1637</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Must be called on a L<Concierge::Users> instance (not a class method). </td> </tr> <tr> <td class="h" > <a name="1638">1638</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1639">1639</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head3 config_to_yaml </td> </tr> <tr> <td class="h" > <a name="1640">1640</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1641">1641</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $yaml = Concierge::Users::Meta::config_to_yaml(\%config, $storage_dir); </td> </tr> <tr> <td class="h" > <a name="1642">1642</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1643">1643</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Converts a configuration hashref to a human-readable YAML string with a </td> </tr> <tr> <td class="h" > <a name="1644">1644</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> warning header. Used internally during C<setup()>. </td> </tr> <tr> <td class="h" > <a name="1645">1645</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1646">1646</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 SEE ALSO </td> </tr> <tr> <td class="h" > <a name="1647">1647</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1648">1648</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Concierge::Users> -- main API and CRUD operations </td> </tr> <tr> <td class="h" > <a name="1649">1649</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1650">1650</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Concierge::Users::Database>, L<Concierge::Users::File>, </td> </tr> <tr> <td class="h" > <a name="1651">1651</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Concierge::Users::YAML> -- storage backend implementations </td> </tr> <tr> <td class="h" > <a name="1652">1652</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1653">1653</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 AUTHOR </td> </tr> <tr> <td class="h" > <a name="1654">1654</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1655">1655</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Bruce Van Allen <bva@cruzio.com> </td> </tr> <tr> <td class="h" > <a name="1656">1656</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1657">1657</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 LICENSE </td> </tr> <tr> <td class="h" > <a name="1658">1658</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1659">1659</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> This module is free software; you can redistribute it and/or modify it </td> </tr> <tr> <td class="h" > <a name="1660">1660</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> under the terms of the Artistic License 2.0. </td> </tr> <tr> <td class="h" > <a name="1661">1661</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1662">1662</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =cut </td> </tr> <tr> <td class="h" > <a name="1663">1663</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="1664">1664</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> __DATA__ </td> </tr> </table> </body> </html>