X-Git-Url: http://erislabs.net/gitweb/?a=blobdiff_plain;f=lib%2FURI%2FTemplate.pm;h=24c01f1261dcc1a4be253d32daa1f37aa5ebb56a;hb=HEAD;hp=d4831b802a701e02405308ae6c7f538714b4127e;hpb=bea2cce0eb375636f4020666cc70ada06766ab23;p=liburi-template-perl.git diff --git a/lib/URI/Template.pm b/lib/URI/Template.pm index d4831b8..24c01f1 100644 --- a/lib/URI/Template.pm +++ b/lib/URI/Template.pm @@ -3,293 +3,416 @@ package URI::Template; use strict; use warnings; -our $VERSION = '0.15'; +our $VERSION = '0.22'; use URI; -use URI::Escape qw(uri_escape_utf8); -use Unicode::Normalize; +use URI::Escape (); +use Unicode::Normalize (); use overload '""' => \&template; -=head1 NAME +my $RESERVED = q(:/?#\[\]\@!\$\&'\(\)\*\+,;=); +my %TOSTRING = ( + '' => \&_tostring, + '+' => \&_tostring, + '#' => \&_tostring, + ';' => \&_tostring_semi, + '?' => \&_tostring_query, + '&' => \&_tostring_query, + '/' => \&_tostring_path, + '.' => \&_tostring_path, +); -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; +} -=head1 DESCRIPTION +sub _quote { + my ( $val, $safe ) = @_; + $safe ||= ''; -This is an initial attempt to provide a wrapper around URI templates -as described at http://www.ietf.org/internet-drafts/draft-gregorio-uritemplate-03.txt + # 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 ); +} -=head1 INSTALLATION +sub _tostring { + my ( $var, $value, $exp ) = @_; + my $safe = $exp->{ safe }; - perl Makefile.PL - make - make test - make install + 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 ); + } -=head1 METHODS + return; +} -=head2 new( $template ) +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 ); + } -Creates a new L instance with the template passed in -as the first parameter. + return; +} -=cut +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 ); + } +} -sub new { - my $class = shift; - my $templ = shift || die 'No template provided'; - my $self = bless { template => $templ, _vars => {} } => $class; - - $self->_study; +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 ); + } - return $self; + return; } sub _study { - my ($self) = @_; + my ( $self ) = @_; my @hunks = grep { defined && length } split /(\{.+?\})/, $self->template; - for (@hunks) { - next unless /^\{(.+?)\}$/; - $_ = $self->_compile_expansion($1); + my $pos = 1; + for ( @hunks ) { + next unless /^\{(.+?)\}$/; + $_ = $self->_compile_expansion( $1, $pos++ ); } - $self->{studied} = \@hunks; + $self->{ studied } = \@hunks; } -sub _op_gen_join { - my ($self, $exp) = @_; - - return sub { - my ($var) = @_; +sub _compile_expansion { + my ( $self, $str, $pos ) = @_; - my @pairs; - for my $keypair (@{ $exp->{vars} }) { - my $key = $keypair->[ 0 ]; - my $val = $keypair->[ 1 ]->( $var ); - next if !exists $var->{$key} && $val eq ''; - Carp::croak "invalid variable ($key) supplied to join operator" - if ref $var->{$key}; + my %exp = ( op => '', vars => [], str => $str ); + if ( $str =~ /^([+#.\/;?&|!\@])(.+)/ ) { + $exp{ op } = $1; + $exp{ str } = $2; + } - push @pairs, $key . '=' . $val; + $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; } - return join $exp->{arg}, @pairs; - }; -} -sub _op_gen_opt { - my ($self, $exp) = @_; + my $join = $exp{ op }; + my $start = $exp{ op }; - Carp::croak "-opt accepts exactly one argument" if @{ $exp->{vars} } != 1; + 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 = ','; + } - my $value = $exp->{arg}; - my $varname = $exp->{vars}->[0]->[0]; + if ( !exists $TOSTRING{ $exp{ op } } ) { + die 'Invalid operation "' . $exp{ op } . '"'; + } return sub { - my ($var) = @_; - return '' unless exists $var->{$varname} and defined $var->{$varname}; - return '' if ref $var->{$varname} and not @{ $var->{$varname} }; + my $variables = shift; - return $value; - }; -} + my @return; + for my $var ( @{ $exp{ vars } } ) { + my $value; + if ( exists $variables->{ $var->{ name } } ) { + $value = $variables->{ $var->{ name } }; + } + $value = $var->{ default } if !defined $value; -sub _op_gen_neg { - my ($self, $exp) = @_; + next unless defined $value; - Carp::croak "-neg accepts exactly one argument" if @{ $exp->{vars} } != 1; + my $expand = $TOSTRING{ $exp{ op } }->( $var, $value, \%exp ); - my $value = $exp->{arg}; - my $varname = $exp->{vars}->[0]->[0]; + push @return, $expand if defined $expand; + } - return sub { - my ($var) = @_; - return $value unless exists $var->{$varname} && defined $var->{$varname}; - return $value if ref $var->{$varname} && ! @{ $var->{$varname} }; - - return ''; + return $start . join( $join, @return ) if @return; + return ''; }; } -sub _op_gen_prefix { - my ($self, $exp) = @_; - - Carp::croak "-prefix accepts exactly one argument" if @{$exp->{vars}} != 1; +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; + } - my $prefix = $exp->{arg}; - my $name = $exp->{vars}->[0]->[0]; + return $self->{ template }; +} - return sub { - my ($var) = @_; - return '' unless exists $var->{$name} && defined $var->{$name}; - my $array = ref $var->{$name} ? $var->{$name} : [ $var->{$name} ]; - return '' unless @$array; +sub variables { + my @vars = sort {$_[ 0 ]->{ _vars }->{ $a } <=> $_[ 0 ]->{ _vars }->{ $b } } keys %{ $_[ 0 ]->{ _vars } }; + return @vars; +} - return join '', map { "$prefix$_" } @$array; - }; +sub expansions { + my $self = shift; + return grep { ref } @{ $self->{ studied } }; } -sub _op_gen_suffix { - my ($self, $exp) = @_; +sub process { + my $self = shift; + return URI->new( $self->process_to_string( @_ ) ); +} - Carp::croak "-suffix accepts exactly one argument" if @{$exp->{vars}} != 1; +sub process_to_string { + my $self = shift; + my $arg = @_ == 1 ? $_[ 0 ] : { @_ }; + my $str = ''; - my $suffix = $exp->{arg}; - my $name = $exp->{vars}->[0]->[0]; + for my $hunk ( @{ $self->{ studied } } ) { + if ( !ref $hunk ) { $str .= $hunk; next; } - return sub { - my ($var) = @_; - return '' unless exists $var->{$name} && defined $var->{$name}; - my $array = ref $var->{$name} ? $var->{$name} : [ $var->{$name} ]; - return '' unless @$array; + $str .= $hunk->( $arg ); + } - return join '', map { "$_$suffix" } @$array; - }; + return $str; } -sub _op_gen_list { - my ($self, $exp) = @_; +1; - Carp::croak "-list accepts exactly one argument" if @{$exp->{vars}} != 1; +__END__ - my $joiner = $exp->{arg}; - my $name = $exp->{vars}->[0]->[0]; +=head1 NAME - return sub { - my ($var) = @_; - return '' unless exists $var->{$name} && defined $var->{$name}; - Carp::croak "variable ($name) used in -list must be an array reference" - unless ref $var->{$name}; +URI::Template - Object for handling URI templates (RFC 6570) - return '' unless my @array = @{ $var->{$name} }; +=head1 SYNOPSIS - return join $joiner, @array; - }; -} + use URI::Template; + + my $template = URI::Template->new( 'http://example.com/{x}' ); + my $uri = $template->process( x => 'y' ); -# not op_gen_* as it is not an op from the spec -sub _op_fill_var { - my( $self, $exp ) = @_; - my( $var, $default ) = split( /=/, $exp, 2 ); - $default = '' if !defined $default; + # 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 $var, sub { - return exists $_[0]->{$var} ? $_[0]->{$var} : $default; - }; -} +=head1 DESCRIPTION -sub _compile_expansion { - my ($self, $str) = @_; +This module provides a wrapper around URI templates as described in RFC 6570: +L<< http://tools.ietf.org/html/rfc6570 >>. - if ($str =~ /\A-([a-z]+)\|(.*?)\|(.+)\z/) { - my $exp = { op => $1, arg => $2, vars => [ map { [ $self->_op_fill_var( $_ ) ] } split /,/, $3 ] }; - $self->{ _vars }->{ $_->[ 0 ] }++ for @{ $exp->{ vars } }; - Carp::croak "unknown expansion operator $exp->{op} in $str" - unless my $code = $self->can("_op_gen_$exp->{op}"); +=head1 INSTALLATION - return $self->$code($exp); - } + perl Makefile.PL + make + make test + make install - # remove "optional" flag (for opensearch compatibility) - $str =~ s{\?$}{}; +=head1 METHODS - my @var = $self->_op_fill_var( $str ); - $self->{ _vars }->{ $var[ 0 ] }++; - return $var[ 1 ]; -} +=head2 new( $template ) -=head2 template +Creates a new L instance with the template passed in +as the first parameter (optional). -This method returns the original template string. +=head2 template( $template ) -=cut - -sub template { - return $_[ 0 ]->{ template }; -} +This method returns the original template string. If provided, it will also set and parse a +new template string. =head2 variables -Returns an array of unique variable names found in the template. NB: they are returned in random order. - -=cut - -sub variables { - return keys %{ $_[ 0 ]->{ _vars } }; -} +Returns an array of unique variable names found in the template (in the order of appearance). =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. -=cut - -sub expansions { - my $self = shift; - return grep { ref } @{ $self->{studied} }; -} - =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. -=cut - -sub process { - my $self = shift; - return URI->new( $self->process_to_string( @_ ) ); -} - =head2 process_to_string( \%vars ) Processes input like the C method, but doesn't inflate the result to a URI object. -=cut +=head1 AUTHORS -sub process_to_string { - my $self = shift; - my $arg = @_ == 1 ? $_[0] : { @_ }; - - my %data; - for my $key (keys %$arg) { - $data{ $key } = ref $arg->{$key} - ? [ map { uri_escape_utf8(NFKC($_)) } @{ $arg->{$key} } ] - : uri_escape_utf8(NFKC($arg->{$key})); - } +=over 4 - my $str = ''; +=item * Brian Cassidy Ebricas@cpan.orgE - for my $hunk (@{ $self->{studied} }) { - if (! ref $hunk) { $str .= $hunk; next; } +=item * Ricardo SIGNES Erjbs@cpan.orgE - $str .= $hunk->(\%data); - } - - return $str; -} - -=head1 AUTHOR - -Brian Cassidy Ebricas@cpan.orgE - -Ricardo SIGNES Erjbs@cpan.orgE +=back =head1 COPYRIGHT AND LICENSE -Copyright 2007-2009 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;