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 )