package Path::Router::Route; our $AUTHORITY = 'cpan:STEVAN'; $Path::Router::Route::VERSION = '0.15'; use B; use Carp 1.32 qw(cluck); use Clone::PP 1.04 (); use Path::Router::Types qw(PathRouterRouteValidationMap); use Types::Standard 1.000005 -types; use Moo 2.000001; use namespace::clean 0.23; # ABSTRACT: An object to represent a route has 'path' => ( is => 'ro', isa => Str, required => 1 ); has 'defaults' => ( is => 'ro', isa => HashRef, default => sub { {} }, predicate => 1, ); has 'validations' => ( is => 'ro', isa => PathRouterRouteValidationMap, coerce => 1, default => sub { {} }, predicate => 1, ); has 'components' => ( is => 'ro', isa => ArrayRef[Str], lazy => 1, default => sub { [ grep {defined && length} split '/' => (shift)->path ] } ); has 'length' => ( is => 'ro', isa => Int, lazy => 1, default => sub { scalar @{(shift)->components} }, ); has 'length_without_optionals' => ( is => 'ro', isa => Int, lazy => 1, default => sub { scalar grep { ! $_[0]->is_component_optional($_) } @{ $_[0]->components } }, ); has 'required_variable_component_names' => ( is => 'ro', isa => ArrayRef[Str], lazy => 1, builder => 1, ); has 'optional_variable_component_names' => ( is => 'ro', isa => ArrayRef[Str], lazy => 1, builder => 1, ); has 'target' => ( # let this just get copied, we # assume cloning of this is not # what you would want is => 'ro', isa => Any, predicate => 'has_target' ); sub BUILD { my $self = shift; return unless $self->has_validations; my %components = map { $self->get_component_name($_) => 1 } grep { $self->is_component_variable($_) } @{ $self->components }; for my $validation (keys %{ $self->validations }) { if (!exists $components{$validation}) { cluck "Validation provided for component :$validation, but the" . " path " . $self->path . " doesn't contain a variable" . " component with that name"; } } } sub _build_required_variable_component_names { my $self = shift; return [ map { $self->get_component_name($_) } grep { $self->is_component_variable($_) && ! $self->is_component_optional($_) } @{ $self->components } ]; } sub _build_optional_variable_component_names { my $self = shift; return [ map { $self->get_component_name($_) } grep { $self->is_component_variable($_) && $self->is_component_optional($_) } @{ $self->components } ]; } # misc sub create_default_mapping { my $self = shift; +{ %{$self->defaults} } } sub has_validation_for { my ($self, $name) = @_; $self->validations->{$name}; } # component checking sub is_component_optional { my ($self, $component) = @_; $component =~ /^\?\:/; } sub is_component_variable { my ($self, $component) = @_; $component =~ /^\??\:/; } sub get_component_name { my ($self, $component) = @_; my ($name) = ($component =~ /^\??\:(.*)$/); return $name; } sub match { my ($self, $parts) = @_; return unless ( @$parts >= $self->length_without_optionals && @$parts <= $self->length ); my @parts = @$parts; # for shifting my $mapping = $self->has_defaults ? $self->create_default_mapping : {}; for my $c (@{ $self->components }) { unless (@parts) { die "should never get here: " . "no \@parts left, but more required components remain" if ! $self->is_component_optional($c); last; } my $part = shift @parts; if ($self->is_component_variable($c)) { my $name = $self->get_component_name($c); if (my $v = $self->has_validation_for($name)) { return unless $v->check($part); } $mapping->{$name} = $part; } else { return unless $c eq $part; } } return Path::Router::Route::Match->new( path => join ('/', @$parts), route => $self, mapping => $mapping, ); } sub generate_match_code { my $self = shift; my $pos = shift; my @regexp; my @variables; foreach my $c (@{$self->components}) { my $re; if ($self->is_component_variable($c)) { $re = "([^\\/]+)"; push @variables, $self->get_component_name($c); } else { $re = $c; $re =~ s/([()])/\\$1/g; } $re = "\\/$re"; if ($self->is_component_optional($c)) { $re = "(?:$re)?"; } push @regexp, $re; } $regexp[0] = '' unless defined $regexp[0]; $regexp[0] =~ s/^\\\///; my $regexp = ''; while (defined(my $piece = pop @regexp)) { $regexp = "(?:$piece$regexp)"; } my @code; push @code, ( '#line ' . __LINE__ . ' "' . __FILE__ . '"', 'printf STDERR "Attempting to match \"' . $self->path . '\" against \"$path\""', 'if Path::Router::DEBUG();', 'print STDERR " regexp is " . ' . B::perlstring($regexp), 'if Path::Router::DEBUG();', 'print STDERR "\n"', 'if Path::Router::DEBUG();', 'if ($path =~ /^' . $regexp . '$/) {', '# ' . $self->path, ); if (@variables) { push @code, ( 'my %captures = (', ); foreach my $i (0..$#variables) { my $name = $variables[$i]; $name =~ s/'/\\'/g; push @code, ( 'defined($' . ($i + 1) . ') ? ' . '(' . B::perlstring($name) . ' => $' . ($i + 1) . ') : (),', ); } push @code, ( ');', ); } push @code, ( 'my $route = $routes->[' . $pos . '];', 'my $valid = 1;', ); if ($self->has_defaults) { push @code, ( 'my $mapping = $route->create_default_mapping;', ); } else { push @code, ( 'my $mapping = {};', ); } if (@variables) { push @code, ( 'my $validations = $route->validations;', 'while (my ($key, $value) = each %captures) {', 'next unless defined $value && length $value;', ); my $if = "if"; foreach my $v (@variables) { if ($self->has_validation_for($v)) { my $vstr = B::perlstring($v); push @code, ( $if . ' ($key eq ' . $vstr . ') {', 'my $v = $validations->{' . $vstr . '};', 'if (!$v->check($value)) {', 'print STDERR ' . $vstr . ' . " failed validation\n"', 'if Path::Router::DEBUG();', '$valid = 0;', '}', '}', ); $if = "elsif"; } } push @code, ( '$mapping->{$key} = $value;', '}', ); } push @code, ( 'if ($valid) {', 'print STDERR "match success\n" if Path::Router::DEBUG();', 'push @matches, bless({', 'path => $path,', 'route => $route,', 'mapping => $mapping,', '}, "Path::Router::Route::Match")', '}', '}', ); return @code; } sub clone { my $self = shift; my %new_args = map {$_ => Clone::PP::clone($self->$_)} qw(path target); if ($self->has_defaults) { $new_args{defaults} = \%{$self->defaults}; } if ($self->has_validations) { $new_args{validations} = \%{$self->validations}; } return ref($self)->new({ %new_args, @_ }); } 1; __END__ =pod =encoding UTF-8 =head1 NAME Path::Router::Route - An object to represent a route =head1 VERSION version 0.15 =head1 DESCRIPTION This object is created by L when you call the C method. In general you won't ever create these objects directly, they will be created for you and you may sometimes introspect them. =head1 METHODS =over 4 =item B $path, ?%options)> =item B =item B =item B =item B =item B =item B =item B =item B =item B =item B =item B =back =over 4 =item B =item B =item B =back =head2 Component checks =over 4 =item B =item B =item B =back =head2 Length methods =over 4 =item B =back =head2 Introspection =over 4 =item B =back =head1 BUGS All complex software has bugs lurking in it, and this module is no exception. If you find a bug please either email me, or add the bug to cpan-RT. =head1 AUTHOR Stevan Little Estevan@cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2008-2011 Infinity Interactive, Inc. L This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =for Pod::Coverage BUILD =head1 AUTHOR Stevan Little =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2016 by Infinity Interactive. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut