Mail-Make

 view release on metacpan or  search on metacpan

lib/MM/Table.pm  view on Meta::CPAN

    for( my $i = 0; $i < scalar( @$src ); $i++ )
    {
        my $e = $src->[ $i ];
        next if( $e->{lkey} ne $lkey );
        return( $e->{val} );
    }

    return;
}

sub merge
{
    my( $self, $key, $val ) = @_;

    $self->_validate_key( $key ) || return( $self->pass_error );
    $val = $self->_stringify_value( $val );
    my $lkey = lc( $key );

    my $st  = $self->_state();
    my $src = $st->{_entries} || [];

    for( my $i = 0; $i < scalar( @$src ); $i++ )
    {
        my $e = $src->[ $i ];
        next if( $e->{lkey} ne $lkey );

        $e->{val} = $e->{val} . ', ' . $val;
        $self->_invalidate_tie_cache();
        return( $self );
    }

    push( @$src, { key => $key, lkey => $lkey, val => $val } );
    $self->_invalidate_tie_cache();
    return( $self );
}

sub overlap
{
    my( $self, $other, $flags ) = @_;

    $self->_validate_table( $other )         || return( $self->pass_error );
    $self->_validate_overlap_flags( $flags ) || return( $self->pass_error );
    $flags //= 0;

    my $o = $other->_state()->{_entries} || [];
    for( my $i = 0; $i < scalar( @$o ); $i++ )
    {
        my $e = $o->[ $i ];
        if( $flags )
        {
            $self->merge( $e->{key}, $e->{val} ) || return( $self->pass_error );
        }
        else
        {
            $self->set( $e->{key}, $e->{val} ) || return( $self->pass_error );
        }
    }
    return( $self );
}

sub overlay
{
    my( $self, $other ) = @_;

    $self->_validate_table( $other ) || return( $self->pass_error );

    my $class = ref( $self ) || $self;

    my $state =
    {
        _entries   => [],
        _error     => undef,
        _tied_href => undef,
    };

    my $new = bless( \$state, $class );

    my $dst = $new->_state()->{_entries};

    my $o = $other->_state()->{_entries} || [];
    for( my $i = 0; $i < scalar( @$o ); $i++ )
    {
        my $e = $o->[ $i ];
        push( @$dst, { key => $e->{key}, lkey => $e->{lkey}, val => $e->{val} } );
    }

    my $s = $self->_state()->{_entries} || [];
    for( my $i = 0; $i < scalar( @$s ); $i++ )
    {
        my $e = $s->[$i];
        push( @$dst, { key => $e->{key}, lkey => $e->{lkey}, val => $e->{val} } );
    }

    return( $new );
}

# pass_error()
# Propagates the error stored in this instance: sets it as the current
# error and returns undef (or empty list), exactly as error() does, but
# without creating a new exception object.
sub pass_error
{
    my $self = shift( @_ );
    # If called with arguments, delegate to error() to create a new exception.
    return( $self->error( @_ ) ) if( @_ );
    # No arguments: the error is already stored in the instance by the method
    # that failed. We simply return, letting Perl resolve the context: undef
    # in scalar context, empty list in list context.
    return;
}

sub set
{
    my( $self, $key, $val ) = @_;

    $self->_validate_key( $key ) || return( $self->pass_error );
    $val = $self->_stringify_value( $val );
    my $lkey = lc( $key );

    my $st  = $self->_state();
    my $src = $st->{_entries} || [];

lib/MM/Table.pm  view on Meta::CPAN

    my( $self ) = @_;
    $self->{_iter} = 0;
    $self->{_curr} = undef;
    return( $self->NEXTKEY( undef ) );
}

sub NEXTKEY
{
    my( $self, $lastkey ) = @_;

    my $src = $self->{_table}->_state()->{_entries} || [];

    if( $self->{_iter} >= scalar( @$src ) )
    {
        $self->{_curr} = undef;
        return( undef );
    }

    my $idx = $self->{_iter};
    $self->{_iter}++;
    $self->{_curr} = $idx;

    return( $src->[$idx]->{key} );
}

sub STORE
{
    my( $self, $key, $val ) = @_;
    return unless( defined( $key ) && !ref( $key ) );
    $self->{_table}->set( $key, $val );
    return;
}

1;
# NOTE: POD
__END__

=encoding utf8

=head1 NAME

MM::Table - Pure-Perl mimic of APR::Table (multi-valued, case-insensitive table)

=head1 SYNOPSIS

    use MM::Table ();
    use MM::Const qw( :table );

    my $t = MM::Table->make;

    $t->set( Foo => "one" ) || die( $t->error );
    $t->add( foo => "two" ) || die( $t->error );

    my $v  = $t->get( 'FOO' );     # "one" (scalar ctx: oldest)
    my @vs = $t->get( 'foo' );     # ("one","two")

    $t->merge( foo => "three" );   # first "foo" becomes "one, three"

    my $copy = $t->copy;

    my $o = $t->overlay( $copy );

    $t->compress( OVERLAP_TABLES_SET );    # flattens to last value per key
    $t->compress( OVERLAP_TABLES_MERGE );  # flattens to "a, b, c"

    $t->do( sub{ print "$_[0] => $_[1]\n"; 1 } ) || die( $t->error );

    # APR-like deref:
    $t->{foo} = "bar";      # calls set()
    print $t->{foo};        # calls get()
    print "yes\n" if( exists( $t->{foo} ) );

    while( my( $k, $v ) = each( %$t ) )
    {
        print "$k => $v\n";    # duplicates preserved in insertion order
    }

=head1 VERSION

    v0.5.0

=head1 DESCRIPTION

A pure-Perl, ordered, multi-valued, case-insensitive key-value table, modelled on L<APR::Table>. Used internally by L<Mail::Make::Headers> to store mail header fields in insertion order while allowing case-insensitive lookup and multiple values per f...

=head1 ERROR HANDLING

C<MM::Table> does not inherit from L<Module::Generic>, but follows the same error convention used throughout the C<Mail::Make> ecosystem:

=over 4

=item * On error, a method stores a L<Mail::Make::Exception> object via L</error> and returns C<undef> in scalar context or an empty list in list context.

=item * The caller retrieves the exception with C<< $t->error >>.

=item * L</pass_error> is provided for propagating an error set earlier in the same object.

=back

Because C<MM::Table> is never instantiated by untrusted input and construction cannot fail, there is no class-level C<< MM::Table->error >> - errors are always per-instance.

=head1 CONSTRUCTOR

=head2 make

    my $t = MM::Table->make;

Creates and returns a new, empty C<MM::Table> instance.

=head1 METHODS

=head2 add( $key, $value )

Appends a new entry without removing any existing entries for C<$key>.
Returns C<$self>, or C<undef> on error.

=head2 clear

Removes all entries. Returns C<$self>.

=head2 compress( $flags )

Flattens duplicate keys. C<$flags> must be C<OVERLAP_TABLES_SET> (C<0>) to keep only the last value, or C<OVERLAP_TABLES_MERGE> (C<1>) to join all values with C<", ">. Returns C<$self>, or C<undef> on error.

=head2 copy

Returns a deep copy of the table as a new C<MM::Table> instance.

=head2 do( $callback [, @filter_keys] )

Iterates over all entries in insertion order, calling C<< $callback->( $key, $value ) >> for each. Iteration stops if the callback returns a false value.
If C<@filter_keys> is provided, only entries whose lowercased key matches one of the filter keys are visited. Returns C<$self>, or C<undef> on error.

=head2 error( [$message] )

Without argument: returns the stored L<Mail::Make::Exception> object, or C<undef> if no error has occurred.

With one or more arguments: joins them into a message, creates a L<Mail::Make::Exception>, stores it, and returns C<undef>.

=head2 get( $key )

In scalar context: returns the value of the first matching entry, or C<undef>. In list context: returns all values for C<$key>, in insertion order. Returns C<undef>/empty list on error.

=head2 merge( $key, $value )

If an entry for C<$key> already exists, appends C<", $value"> to its value.
Otherwise behaves like L</add>. Returns C<$self>, or C<undef> on error.

=head2 overlap( $other_table, $flags )

Copies all entries from C<$other_table> into C<$self>. With C<OVERLAP_TABLES_SET> each key is replaced; with C<OVERLAP_TABLES_MERGE> values are appended. Returns C<$self>, or C<undef> on error.

=head2 overlay( $other_table )

Returns a new C<MM::Table> containing all entries from C<$other_table> followed by all entries from C<$self> (C<$other_table> entries come first).
Returns the new table, or C<undef> on error.

=head2 pass_error

Propagates the error currently stored in this instance by returning C<undef>. If called with arguments, delegates to L</error> to create a new exception first.

=head2 set( $key, $value )

Removes all existing entries for C<$key> and adds a single new one.
Returns C<$self>, or C<undef> on error.

=head2 unset( $key )

Removes all entries for C<$key>. Returns C<$self>, or C<undef> on error.

=head1 TIED-HASH INTERFACE

C<MM::Table> overloads C<%{}> to expose a tied hash interface compatible with APR::Table's C<< $t->{key} >> syntax. Assignment calls L</set>, deletion calls L</unset>, and C<each>/C<keys>/C<values> iterate in insertion order. Multiple values for the ...

=head1 NOTES / LIMITATIONS

=over 4

=item * Performance

All lookups are linear scans. C<MM::Table> is designed for the small, bounded sets of headers found in email messages, not for large tables.

=item * C<copy> and the C<$pool> argument

The C<copy> method accepts no arguments. The C<$pool> parameter present in the original C<APR::Table> API has no equivalent here.

=back

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 SEE ALSO

L<APR::Table>, L<Mail::Make::Headers>, L<Mail::Make::Exception>

=head1 COPYRIGHT & LICENSE

Copyright(c) 2026 DEGUEST Pte. Ltd.

All rights reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut



( run in 2.304 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )