Concierge-Users
view release on metacpan or search on metacpan
lib/Concierge/Users/Meta.pm view on Meta::CPAN
# Start with built-in field definitions (clone to avoid modifying master hash)
my %merged_definitions = map {
$_ => { $Concierge::Users::Meta::field_definitions{$_}->%* }
} @core_fields, @system_fields;
# Add requested standard fields
my @included_std_fields;
my @requested_fields;
if ( !$config->{include_standard_fields} or $config->{include_standard_fields} =~ /^all$/i ) {
@included_std_fields = @standard_fields;
}
else {
if ( ref $config->{include_standard_fields} eq 'ARRAY' ) {
@requested_fields = map { lc $_ } $config->{include_standard_fields}->@*;
}
elsif ( ! ref $config->{include_standard_fields} ) {
@requested_fields = map { lc $_ } split /\s*[,;]\s*/ => $config->{include_standard_fields};
}
my %standard_fields = map { $_ => 1 } @standard_fields;
for my $fld (@requested_fields) {
if ($standard_fields{$fld}) {
push @included_std_fields => $fld;
}
else {
carp "Non-standard field requested: $fld; configure with 'app_fields => [ ...]'";
}
}
}
push @fields, @included_std_fields;
for my $fld (@included_std_fields) {
$merged_definitions{$fld} = { $Concierge::Users::Meta::field_definitions{$fld}->%* }
if $Concierge::Users::Meta::field_definitions{$fld};
}
# Process field_overrides - modify built-in field definitions
# Protected fields (cannot be overridden): user_id, created_date, last_mod_date
# Protected attributes (cannot be changed): field_name, category
if ($config->{field_overrides}) {
# my @protected_fields = qw/ user_id created_date last_mod_date /;
# my %protected_fields = map { $_ => 1 } @protected_fields;
# my @protected_attrs = qw/ field_name category /;
# my %protected_attrs = map { $_ => 1 } @protected_attrs;
my @overrides = ref $config->{field_overrides} eq 'ARRAY'
? $config->{field_overrides}->@*
: ();
foreach my $override (@overrides) {
next unless ref $override eq 'HASH';
my $field_name = $override->{field_name};
unless ($field_name) {
carp "Field override missing field_name; skipping";
next;
}
# Check if field is protected; only format_as and label may be overridden
if ( $field_name =~ /^(?:user_id|last_login_date|last_mod_date|created_date)$/) {
my %allowed = map { $_ => 1 } qw/format_as label/;
my @apply = grep { $allowed{$_} } keys %$override;
my @blocked = grep { !$allowed{$_} && $_ ne 'field_name' } keys %$override;
carp "Field '$field_name' is protected; ignoring: " . join(', ', sort @blocked)
if @blocked;
$merged_definitions{$field_name}{$_} = $override->{$_} for @apply;
next;
}
# Check if field exists in merged_definitions
unless ($merged_definitions{$field_name}) {
carp "Cannot override unknown field '$field_name'; field must be included via include_standard_fields or app_fields";
next;
}
# Process each attribute in the override
my %warnings;
foreach my $attr (keys %$override) {
# Skip field_name itself (it's the identifier, not an attribute to override)
next if $attr eq 'field_name';
# Skip protected attributes
# if ($protected_attrs{$attr}) {
if ($attr =~ /field_name|category/) {
$warnings{$attr} = "protected attribute '$attr' cannot be changed";
next;
}
# Validate validate_as against known types
if ($attr eq 'validate_as') {
my $validator_type = $override->{$attr};
# unless ($known_validators{$validator_type}) {
unless ($Concierge::Users::Meta::type_validator_map{$validator_type}) {
$warnings{$attr} = "unknown validator type '$validator_type' - falling back to 'text'";
$merged_definitions{$field_name}{$attr} = 'text';
next;
}
}
# Apply the override
$merged_definitions{$field_name}{$attr} = $override->{$attr};
}
# Auto-update validate_as when type is changed (unless validate_as was also explicitly overridden)
if (exists $override->{type} && !exists $override->{validate_as}) {
my $new_type = $override->{type};
# if ($known_validators{$new_type}) {
if ($Concierge::Users::Meta::type_validator_map{$new_type}) {
$merged_definitions{$field_name}{validate_as} = $new_type;
}
}
# Auto-update must_validate when required is set to 1 (unless must_validate was explicitly overridden)
if (exists $override->{required} && $override->{required} == 1 && !exists $override->{must_validate}) {
$merged_definitions{$field_name}{must_validate} = 1;
}
# Emit warnings if any
if (%warnings) {
my $warning_list = join(', ', map { "$_: $warnings{$_}" } sort keys %warnings);
carp "Field '$field_name' override: $warning_list";
}
}
}
lib/Concierge/Users/Meta.pm view on Meta::CPAN
A field's C<type> declares its data type and determines the default
validator. C<validate_as> overrides the validator without changing
the type. For example, an application field with C<< type => 'text' >>
and C<< validate_as => 'moniker' >> is stored as text but validated with
the moniker pattern. When C<type> is changed via a field override and
C<validate_as> is not explicitly set, C<validate_as> is updated
automatically to match the new type.
=head2 must_validate Behavior
When C<must_validate> is C<1> for a field, a validation failure causes
the entire C<register_user> or C<update_user> call to return
C<< { success => 0 } >>. When C<must_validate> is C<0>, the field's
value is silently dropped and a warning is appended to the response.
Setting C<< required => 1 >> in a field override automatically enables
C<must_validate> unless C<must_validate> is explicitly set in the same
override.
The environment variable C<USERS_SKIP_VALIDATION> bypasses all
validation when set to a true value.
=head1 FIELD CUSTOMIZATION
=head2 Application Fields
Pass C<app_fields> to C<< Concierge::Users->setup() >> as an arrayref.
Each element is either a string (minimal definition) or a hashref (full
definition):
app_fields => [
'nickname', # string shorthand
{ # full definition
field_name => 'department',
type => 'enum',
options => ['*Engineering', 'Sales', 'Support'],
required => 1,
label => 'Department',
},
],
String shorthand creates a field with C<< type => 'text' >>,
C<< validate_as => 'text' >>, C<< required => 0 >>.
Reserved names (any core, standard, or system field name) are rejected
with a warning.
=head2 Field Overrides
Pass C<field_overrides> to C<setup()> as an arrayref of hashrefs.
Each must contain C<field_name> to identify the target:
field_overrides => [
{
field_name => 'email',
required => 1,
label => 'Work Email',
},
],
B<Protected fields> (structural attributes blocked; C<format_as> and
C<label> are allowed): C<user_id>, C<last_login_date>, C<last_mod_date>,
C<created_date>.
B<Protected attributes> that cannot be changed: C<field_name>,
C<category>.
Auto-behaviors:
=over 4
=item * Changing C<type> auto-updates C<validate_as> to match (unless
C<validate_as> is also specified).
=item * Setting C<< required => 1 >> auto-enables C<must_validate>
(unless C<must_validate> is also specified).
=item * An unknown C<validate_as> value falls back to C<text> with a
warning.
=back
B<Overriding enum options:> Core fields like C<user_status> and
C<access_level> are always present, but their C<options> are not
fixed. Replace them with values that fit your domain:
# Makerspace member status instead of the default
# Eligible / OK / Inactive
field_overrides => [
{
field_name => 'user_status',
options => [qw( *Applicant Novice Skilled
Expert Mentor Steward )],
},
],
The C<*>-prefixed option becomes the default (see L</Enum Default
Convention>). Validation, filtering, and all other enum behaviors
apply to the new option set automatically.
=head2 Enum Default Convention
In an C<options> arrayref, prefix exactly one value with C<*> to mark it
as the default:
options => ['*Free', 'Premium', 'Enterprise']
The C<*> is stripped for validation (stored internally in C<v_options>).
If no explicit C<default> is set for the field, the C<*>-marked option
becomes the default automatically. A bare C<*> (e.g. in C<prefix> and
C<suffix>) represents an empty default.
=head1 FILTER DSL
The C<list_users> method accepts a filter string with five operators and
two combinators.
=head2 Operators
= exact match user_status=OK
: substring (case-insensitive) last_name:smith
( run in 0.826 second using v1.01-cache-2.11-cpan-800906f7e73 )