use strict;
use warnings;
-our $VERSION = '0.13';
+our $VERSION = '0.22';
use URI;
-use URI::Escape ();
-use overload '""' => \&as_string;
+use URI::Escape ();
+use Unicode::Normalize ();
+use overload '""' => \&template;
+
+my $RESERVED = q(:/?#\[\]\@!\$\&'\(\)\*\+,;=);
+my %TOSTRING = (
+ '' => \&_tostring,
+ '+' => \&_tostring,
+ '#' => \&_tostring,
+ ';' => \&_tostring_semi,
+ '?' => \&_tostring_query,
+ '&' => \&_tostring_query,
+ '/' => \&_tostring_path,
+ '.' => \&_tostring_path,
+);
-my $unsafe = q(^A-Za-z0-9\-_.~!\$\&'()*+,;=:/?\[\]#@);
-
-=head1 NAME
-
-URI::Template - Object for handling URI templates
+sub new {
+ my $class = shift;
+ my $templ = shift;
+ $templ = '' unless defined $templ;
+ my $self = bless { template => $templ, _vars => {} } => $class;
-=head1 SYNOPSIS
+ $self->_study;
- use URI::Template;
- my $template = URI::Template->new( 'http://example.com/{x}' );
- my $uri = $template->process( x => 'y' );
- # uri is a URI object with value 'http://example.com/y'
+ return $self;
+}
- my %result = $template->deparse( $uri );
- # %result is ( x => 'y' )
+sub _quote {
+ my ( $val, $safe ) = @_;
+ $safe ||= '';
-=head1 DESCRIPTION
+ # try to mirror python's urllib quote
+ my $unsafe = '^A-Za-z0-9\-\._' . $safe;
+ return URI::Escape::uri_escape_utf8( Unicode::Normalize::NFKC( $val ),
+ $unsafe );
+}
-This is an initial attempt to provide a wrapper around URI templates
-as described at http://www.ietf.org/internet-drafts/draft-gregorio-uritemplate-01.txt
+sub _tostring {
+ my ( $var, $value, $exp ) = @_;
+ my $safe = $exp->{ safe };
-=head1 INSTALLATION
+ if ( ref $value eq 'ARRAY' ) {
+ return join( ',', map { _quote( $_, $safe ) } @$value );
+ }
+ elsif ( ref $value eq 'HASH' ) {
+ return join(
+ ',',
+ map {
+ _quote( $_, $safe )
+ . ( $var->{ explode } ? '=' : ',' )
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ elsif ( defined $value ) {
+ return _quote(
+ substr( $value, 0, $var->{ prefix } || length( $value ) ),
+ $safe );
+ }
- perl Makefile.PL
- make
- make test
- make install
+ return;
+}
-=head1 METHODS
+sub _tostring_semi {
+ my ( $var, $value, $exp ) = @_;
+ my $safe = $exp->{ safe };
+ my $join = $exp->{ op };
+ $join = '&' if $exp->{ op } eq '?';
+
+ if ( ref $value eq 'ARRAY' ) {
+ if ( $var->{ explode } ) {
+ return join( $join,
+ map { $var->{ name } . '=' . _quote( $_, $safe ) } @$value );
+ }
+ else {
+ return $var->{ name } . '='
+ . join( ',', map { _quote( $_, $safe ) } @$value );
+ }
+ }
+ elsif ( ref $value eq 'HASH' ) {
+ if ( $var->{ explode } ) {
+ return join(
+ $join,
+ map {
+ _quote( $_, $safe ) . '='
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ else {
+ return $var->{ name } . '=' . join(
+ ',',
+ map {
+ _quote( $_, $safe ) . ','
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ }
+ elsif ( defined $value ) {
+ return $var->{ name } unless length( $value );
+ return
+ $var->{ name } . '='
+ . _quote(
+ substr( $value, 0, $var->{ prefix } || length( $value ) ),
+ $safe );
+ }
-=head2 new( $template )
+ return;
+}
-Creates a new L<URI::Template> instance with the template passed in
-as the first parameter.
+sub _tostring_query {
+ my ( $var, $value, $exp ) = @_;
+ my $safe = $exp->{ safe };
+ my $join = $exp->{ op };
+ $join = '&' if $exp->{ op } =~ /[?&]/;
+
+ if ( ref $value eq 'ARRAY' ) {
+ return if !@$value;
+ if ( $var->{ explode } ) {
+ return join( $join,
+ map { $var->{ name } . '=' . _quote( $_, $safe ) } @$value );
+ }
+ else {
+ return $var->{ name } . '='
+ . join( ',', map { _quote( $_, $safe ) } @$value );
+ }
+ }
+ elsif ( ref $value eq 'HASH' ) {
+ return if !keys %$value;
+ if ( $var->{ explode } ) {
+ return join(
+ $join,
+ map {
+ _quote( $_, $safe ) . '='
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ else {
+ return $var->{ name } . '=' . join(
+ ',',
+ map {
+ _quote( $_, $safe ) . ','
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ }
+ elsif ( defined $value ) {
+ return $var->{ name } . '=' unless length( $value );
+ return
+ $var->{ name } . '='
+ . _quote(
+ substr( $value, 0, $var->{ prefix } || length( $value ) ),
+ $safe );
+ }
+}
-=cut
+sub _tostring_path {
+ my ( $var, $value, $exp ) = @_;
+ my $safe = $exp->{ safe };
+ my $join = $exp->{ op };
+
+ if ( ref $value eq 'ARRAY' ) {
+ return unless @$value;
+ return join(
+ ( $var->{ explode } ? $join : ',' ),
+ map { _quote( $_, $safe ) } @$value
+ );
+ }
+ elsif ( ref $value eq 'HASH' ) {
+ return join(
+ ( $var->{ explode } ? $join : ',' ),
+ map {
+ _quote( $_, $safe )
+ . ( $var->{ explode } ? '=' : ',' )
+ . _quote( $value->{ $_ }, $safe )
+ } sort keys %$value
+ );
+ }
+ elsif ( defined $value ) {
+ return _quote(
+ substr( $value, 0, $var->{ prefix } || length( $value ) ),
+ $safe );
+ }
-sub new {
- my $class = shift;
- my $templ = shift || die 'No template provided';
- my $self = bless { template => $templ }, $class;
+ return;
+}
- return $self;
+sub _study {
+ my ( $self ) = @_;
+ my @hunks = grep { defined && length } split /(\{.+?\})/, $self->template;
+ my $pos = 1;
+ for ( @hunks ) {
+ next unless /^\{(.+?)\}$/;
+ $_ = $self->_compile_expansion( $1, $pos++ );
+ }
+ $self->{ studied } = \@hunks;
}
-=head2 as_string( )
+sub _compile_expansion {
+ my ( $self, $str, $pos ) = @_;
-Returns the original template string. Also used when the object is
-stringified.
+ my %exp = ( op => '', vars => [], str => $str );
+ if ( $str =~ /^([+#.\/;?&|!\@])(.+)/ ) {
+ $exp{ op } = $1;
+ $exp{ str } = $2;
+ }
-=cut
+ $exp{ safe } = $RESERVED if $exp{ op } =~ m{[+#]};
+
+ for my $varspec ( split( ',', delete $exp{ str } ) ) {
+ my %var = ( name => $varspec );
+ if ( $varspec =~ /=/ ) {
+ @var{ 'name', 'default' } = split( /=/, $varspec, 2 );
+ }
+ if ( $var{ name } =~ s{\*$}{} ) {
+ $var{ explode } = 1;
+ }
+ elsif ( $var{ name } =~ /:/ ) {
+ @var{ 'name', 'prefix' } = split( /:/, $var{ name }, 2 );
+ if ( $var{ prefix } =~ m{[^0-9]} ) {
+ die 'Non-numeric prefix specified';
+ }
+ }
+
+ # remove "optional" flag (for opensearch compatibility)
+ $var{ name } =~ s{\?$}{};
+ $self->{ _vars }->{ $var{ name } } = $pos;
+
+ push @{ $exp{ vars } }, \%var;
+ }
-sub as_string {
- return $_[ 0 ]->{ template };
-}
+ my $join = $exp{ op };
+ my $start = $exp{ op };
-=head2 variables( )
+ if ( $exp{ op } eq '+' ) {
+ $start = '';
+ $join = ',';
+ }
+ elsif ( $exp{ op } eq '#' ) {
+ $join = ',';
+ }
+ elsif ( $exp{ op } eq '?' ) {
+ $join = '&';
+ }
+ elsif ( $exp{ op } eq '&' ) {
+ $join = '&';
+ }
+ elsif ( $exp{ op } eq '' ) {
+ $join = ',';
+ }
-Returns an array of unique variable names found in the template.
-NB: they are returned in random order.
+ if ( !exists $TOSTRING{ $exp{ op } } ) {
+ die 'Invalid operation "' . $exp{ op } . '"';
+ }
-=cut
+ return sub {
+ my $variables = shift;
-sub variables {
- my $self = shift;
- my %vars = map { $_ => 1 } $self->all_variables;
- return keys %vars;
-}
+ my @return;
+ for my $var ( @{ $exp{ vars } } ) {
+ my $value;
+ if ( exists $variables->{ $var->{ name } } ) {
+ $value = $variables->{ $var->{ name } };
+ }
+ $value = $var->{ default } if !defined $value;
-=head2 all_variables( )
+ next unless defined $value;
-Returns an array of variable names found as they appear in template --
-in order, duplicates included.
+ my $expand = $TOSTRING{ $exp{ op } }->( $var, $value, \%exp );
-=cut
+ push @return, $expand if defined $expand;
+ }
-sub all_variables {
- my $self = shift;
- my @vars = $self->as_string =~ /{(.+?)}/g;
- return @vars;
+ return $start . join( $join, @return ) if @return;
+ return '';
+ };
}
-=head2 process( %vars|\@values )
+sub template {
+ my $self = shift;
+ my $templ = shift;
+
+ # Update template
+ if ( defined $templ && $templ ne $self->{ template } ) {
+ $self->{ template } = $templ;
+ $self->{ _vars } = {};
+ $self->_study;
+ return $self;
+ }
-Given a list of key-value pairs or an array ref of values (for
-positional substitution), it will URI escape the values and
-substitute them in to the template. Returns a URI object.
+ return $self->{ template };
+}
-=cut
+sub variables {
+ my @vars = sort {$_[ 0 ]->{ _vars }->{ $a } <=> $_[ 0 ]->{ _vars }->{ $b } } keys %{ $_[ 0 ]->{ _vars } };
+ return @vars;
+}
+
+sub expansions {
+ my $self = shift;
+ return grep { ref } @{ $self->{ studied } };
+}
sub process {
my $self = shift;
return URI->new( $self->process_to_string( @_ ) );
}
-=head2 process_to_string( %vars|\@values )
-
-Processes input like the C<process> method, but doesn't
-inflate the result to a URI object.
-
-=cut
-
sub process_to_string {
my $self = shift;
+ my $arg = @_ == 1 ? $_[ 0 ] : { @_ };
+ my $str = '';
- if ( ref $_[ 0 ] ) {
- return $self->_process_by_position( @_ );
- }
- else {
- return $self->_process_by_key( @_ );
+ for my $hunk ( @{ $self->{ studied } } ) {
+ if ( !ref $hunk ) { $str .= $hunk; next; }
+
+ $str .= $hunk->( $arg );
}
+
+ return $str;
}
-sub _process_by_key {
- my $self = shift;
- my @vars = $self->variables;
- my %params = @_;
- my $uri = $self->as_string;
+1;
- # fix undef vals
- for my $var ( @vars ) {
- $params{ $var }
- = defined $params{ $var }
- ? URI::Escape::uri_escape( $params{ $var }, $unsafe )
- : '';
- }
+__END__
- my $regex = '\{(' . join( '|', map quotemeta, @vars ) . ')\}';
- $uri =~ s/$regex/$params{$1}/eg;
+=head1 NAME
- return $uri;
-}
+URI::Template - Object for handling URI templates (RFC 6570)
-sub _process_by_position {
- my $self = shift;
- my @params = @{ $_[ 0 ] };
+=head1 SYNOPSIS
- my $uri = $self->as_string;
+ use URI::Template;
+
+ my $template = URI::Template->new( 'http://example.com/{x}' );
+ my $uri = $template->process( x => 'y' );
- $uri =~ s/{(.+?)}/@params
- ? defined $params[ 0 ]
- ? URI::Escape::uri_escape( shift @params, $unsafe )
- : ''
- : ''/eg;
+ # or
+
+ my $template = URI::Template->new();
+ $template->template( 'http://example.com/{x}' );
+ my $uri = $template->process( x => 'y' );
+
+ # uri is a URI object with value 'http://example.com/y'
- return $uri;
-}
+=head1 DESCRIPTION
-=head2 deparse( $uri )
+This module provides a wrapper around URI templates as described in RFC 6570:
+L<< http://tools.ietf.org/html/rfc6570 >>.
-Does some rudimentary deparsing of a uri based on the current template.
-Returns a hash with the extracted values.
+=head1 INSTALLATION
-=cut
+ perl Makefile.PL
+ make
+ make test
+ make install
-sub deparse {
- my $self = shift;
- my $uri = shift;
+=head1 METHODS
- if ( !$self->{ deparse_re } ) {
- my $templ = $self->as_string;
- $self->{ vars_list } = [ $templ =~ /{(.+?)}/g ];
- $templ =~ s/{.+?}/(.+?)/g;
- $self->{ deparse_re } = qr/^${templ}$/;
- }
+=head2 new( $template )
- my @matches = $uri =~ $self->{ deparse_re };
+Creates a new L<URI::Template> instance with the template passed in
+as the first parameter (optional).
- my %results;
- @results{ @{ $self->{ vars_list } } } = @matches;
- return %results;
-}
+=head2 template( $template )
+
+This method returns the original template string. If provided, it will also set and parse a
+new template string.
+
+=head2 variables
-=head1 AUTHOR
+Returns an array of unique variable names found in the template (in the order of appearance).
-Brian Cassidy E<lt>bricas@cpan.orgE<gt>
+=head2 expansions
+
+This method returns an list of expansions found in the template. Currently,
+these are just coderefs. In the future, they will be more interesting.
+
+=head2 process( \%vars )
+
+Given a list of key-value pairs or an array ref of values (for
+positional substitution), it will URI escape the values and
+substitute them in to the template. Returns a URI object.
+
+=head2 process_to_string( \%vars )
+
+Processes input like the C<process> method, but doesn't inflate the result to a
+URI object.
+
+=head1 AUTHORS
+
+=over 4
+
+=item * Brian Cassidy E<lt>bricas@cpan.orgE<gt>
+
+=item * Ricardo SIGNES E<lt>rjbs@cpan.orgE<gt>
+
+=back
=head1 COPYRIGHT AND LICENSE
-Copyright 2008 by Brian Cassidy
+Copyright 2007-2015 by Brian Cassidy
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
-
-1;