package MVC::Neaf::Util::Container;
use strict;
use warnings;
our $VERSION = '0.28';
=head1 NAME
MVC::Neaf::Util::Container - path & method based container for Not Even A Framework
=head1 DESCRIPTION
This is utility class.
Nothing to see here unless one intends to work on L<MVC::Neaf> itself.
This class can hold multiple entities addressed by paths and methods
and extract them in the needed order.
=head1 SYNOPSIS
my $c = MVC::Neaf::Util::Container->new;
$c->store( "foo", path => '/foo', method => 'GET' );
$c->store( "bar", path => '/foo/bar', exclude => '/foo/bar/baz' );
$c->fetch( path => "/foo", method => 'GET' ); # foo
$c->fetch( path => "/foo/bar", method => 'GET' ); # foo bar
$c->fetch( path => "/foo/bar", method => 'POST' );
# qw(bar) - 'foo' limited to GET only
$c->fetch( path => "/foo/bar/baz", method => 'GET' );
# qw(foo) - 'bar' excluded
=cut
use Carp;
use parent qw(MVC::Neaf::Util::Base);
use MVC::Neaf::Util qw( maybe_list canonize_path path_prefixes supported_methods check_path );
our @CARP_NOT = qw(MVC::Neaf::Route);
=head1 ATTRIBUTES
=head2 exclusive
Only store one item per (path,method) pair, and fail loudly in case of conflicts.
=head1 METHODS
=head2 store
store( $data, %spec )
Store $data in container. Spec may include:
=over
=item path - single path or list of paths, '/' assumed if none.
=item method - name of method or array of methods.
By default, all methods supported by Neaf.
=item exclude - single path or list of paths. None by default.
=item prepend - if true, prepend to the list instead of appending.
=item tentative (exclusive container only) - if true, don't override existing
declarations, and don't complain when overridden.
=item override (exclusive container only) - if true, override
any preexisting content.
=back
=cut
sub store {
my ($self, $data, %opt) = @_;
$self->my_croak( "'tentative' and 'override' are useless for non-exclusive container" )
if !$self->{exclusive} and ( $opt{tentative} or $opt{override} );
$self->my_croak( "'tentative' and 'override' are mutually exclusive" )
if $opt{tentative} and $opt{override};
$opt{data} = $data;
my @methods = map { uc $_ } maybe_list( $opt{method}, supported_methods() );
my @todo = check_path map { canonize_path( $_ ) } maybe_list( $opt{path}, '' );
if ($opt{exclude}) {
my $rex = join '|', map { quotemeta(canonize_path($_)) }
check_path maybe_list( $opt{exclude} );
$opt{exclude} = qr(^(?:$rex)(?:[/?]|$));
@todo = grep { $_ !~ $opt{exclude} } @todo
};
if ($self->{exclusive}) {
my @list = $self->store_check_conflict( %opt, method => \@methods, path => \@todo );
$self->my_croak( "Conflicting path spec: ".join ", ", @list )
if @list;
};
foreach my $method ( @methods ) {
foreach my $path ( @todo ) {
my $array = $self->{data}{$method}{$path} ||= [];
if ( $self->{exclusive} ) {
@$array = (\%opt)
unless $array->[0] and $opt{tentative} and !$array->[0]{tentative};
} elsif ( $opt{prepend} ) {
unshift @$array, \%opt;
} else {
push @$array, \%opt;
};
};
};
$self;
};
=head2 store_check_conflict
store_check_conflict( path => ..., method => ... )
Check that no previous declarations conflict with the new one.
This is only if exclusive was specified.
=cut
sub store_check_conflict {
my ($self, %opt) = @_;
$self->my_croak( "useless call for non-exclusive container" )
unless $self->{exclusive};
if (!$opt{tentative} and !$opt{override}) {
# Check for conflicts before changing anything
my %conflict;
foreach my $method ( @{ $opt{method} } ) {
foreach my $path ( @{ $opt{path} } ) {
my $existing = $self->{data}{$method}{$path};
next unless $existing && $existing->[0];
next if $existing->[0]->{tentative};
push @{ $conflict{$path} }, $method;
};
};
my @list =
map { $_."[".(join ",", sort @{ $conflict{$_} })."]" }
sort keys %conflict;
return @list;
};
return ();
};
=head2 list_methods
Returns methods currently in the storage.
=cut
sub list_methods {
my $self = shift;
return keys %{ $self->{data} };
};
=head2 list_paths
Returns paths for given method, or all if no method given.
=cut
sub list_paths {
my ($self, @methods) = @_;
@methods = $self->list_methods
unless @methods;
my %uniq;
foreach my $method (@methods) {
$uniq{$_}++ for keys %{ $self->{data}{$method} };
};
return keys %uniq;
};
=head2 fetch
fetch( %spec )
Return all matching previously stored objects,
from shorter to longer paths, in order of addition.
Spec may include:
=over
=item path - a single path to match against
=item method - method to match against
=back
=cut
sub fetch {
my $self = shift;
return map { $_->{data} } $self->fetch_raw(@_);
};
=head2 fetch_last
Same as fetch(), but only return the last (last added & longest path) element.
=cut
sub fetch_last {
my $self = shift;
my ($bucket) = reverse $self->fetch_raw(@_);
return $bucket->{data};
};
=head2 fetch_raw
Same as fetch(), but return additional info instead of just stored item:
{
data => $your_item_here,
path => $all_the_paths,
method => $list_of_methods,
...
}
=cut
sub fetch_raw {
my ($self, %opt) = @_;
my @missing = grep { !defined $opt{$_} } qw(path method);
croak __PACKAGE__."->fetch: required fields missing: @missing"
if @missing;
my $path = canonize_path( $opt{path} );
my @ret;
my $tree = $self->{data}{ $opt{method} };
foreach my $prefix ( path_prefixes( $opt{path} || '' ) ) {
my $list = $tree->{$prefix};
next unless $list;
foreach my $node( @$list ) {
next if $node->{exclude} and $opt{path} =~ $node->{exclude};
push @ret, $node;
};
};
return @ret;
};
=head1 LICENSE AND COPYRIGHT
This module is part of L<MVC::Neaf> suite.
Copyright 2016-2019 Konstantin S. Uvarin C<khedin@cpan.org>.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See L<http://dev.perl.org/licenses/> for more information.
=cut
1;