From 876148a219d6044ae8039ae4145bebc711b34951 Mon Sep 17 00:00:00 2001 From: Jakob Voss Date: Tue, 14 Jun 2011 17:16:19 +0200 Subject: [PATCH] also detect format via extension --- .gitignore | 3 + dist.ini | 1 + example/app.psgi | 8 +- lib/RDF/Light.pm | 182 +++++++++++++++++++++-------------- lib/RDF/Light/Graph.pm | 11 +-- lib/RDF/Light/Source.pm | 82 +++++++++------- t/30_sources.t | 20 ++++ t/40_formats.t | 82 ++++++++++++++++ t/{40_graph.t => 50_graph.t} | 0 t/TestPlackApp.pm | 19 +++- t/template.t | 5 +- 11 files changed, 291 insertions(+), 122 deletions(-) create mode 100644 t/40_formats.t rename t/{40_graph.t => 50_graph.t} (100%) diff --git a/.gitignore b/.gitignore index b25c15b..10c6e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *~ +.build +*.tar.gz +RDF-Light-* diff --git a/dist.ini b/dist.ini index 17deb30..92c26d2 100644 --- a/dist.ini +++ b/dist.ini @@ -27,3 +27,4 @@ bugtracker.web = https://github.com/nichtich/RDF-Light/issues [PodSyntaxTests] [AutoPrereqs] +skip = ^TestPlackApp|RDF::Light::Node$ diff --git a/example/app.psgi b/example/app.psgi index 7224f52..1ba05ec 100644 --- a/example/app.psgi +++ b/example/app.psgi @@ -97,7 +97,7 @@ my $index_app = sub { $tt->process("index.html", $vars, \$content); utf8::downgrade($content); - $env->{'psgix.logger'}->({ level => "info", message => "Index done:$content" }); + #$env->{'psgix.logger'}->({ level => "info", message => "Index done:$content" }); return [ 200, ['Content-Type' => 'text/html'], [$content]]; }; @@ -106,9 +106,9 @@ my $index_app = sub { builder { enable 'SimpleLogger'; enable 'Debug'; - enable 'Plack::Middleware::Static', root => $dir, path => qr/\.css$/; - enable 'JSONP'; # for RDF/JSON in AJAX - enable "+RDF::Light", source => $model, base => $base; + enable 'JSONP'; # to support RDF/JSON in AJAX + enable 'Static', root => $dir, path => qr/\.css$/; + enable '+RDF::Light', source => $model, base => $base; enable $index_app; $app; }; diff --git a/lib/RDF/Light.pm b/lib/RDF/Light.pm index 21126b1..ad1e789 100644 --- a/lib/RDF/Light.pm +++ b/lib/RDF/Light.pm @@ -1,7 +1,6 @@ -package RDF::Light; - use strict; use warnings; +package RDF::Light; =head1 NAME @@ -28,38 +27,6 @@ RDF::Light - Simplified Linked Data handling $app; } -=head1 INTRODUCTION - -This package provides a PSGI application to serve RDF as Linked Data. In -contrast to other Linked Data applications, URIs must not have query parts and -the distinction between information-resources and non-information resources is -disregarded (some Semantic Web evangelists may be angry about this). By now -this package is experimental. For a more complete package see -L. - -The package implements a PSGI application that can be used as -L to provide RDF data. The implementation is based on -L which is a full implementation of RDF standards in Perl. - -=head1 OVERVIEW - -An RDF::Light application processes PSGI/HTTP requests in three steps: - -=over 4 - -=item 1 - -Determine query URI and serialization format (mime type) and set the request -variables C, C, and C. - -=item 2 - -Retrieve data about the resource which is identified by the request URI. - -=item 3 - -Create a serialization. - =cut use Try::Tiny; @@ -71,7 +38,7 @@ use Carp; use RDF::Light::Source; use parent 'Plack::Middleware'; -use Plack::Util::Accessor qw(source formats base); +use Plack::Util::Accessor qw(source base formats via_param via_extension); use parent 'Exporter'; our @EXPORT_OK = qw(guess_serialization); @@ -91,14 +58,13 @@ our %rdf_formats = ( sub prepare_app { my $self = shift; + $self->formats( \%rdf_formats ) unless $self->formats; + # TODO: support array ref and custom serialization formats - if ( $self->formats ) { - ref $self->formats eq 'HASH' or carp 'formats must be a hash reference'; - } else { - $self->formats( \%rdf_formats ); - } + ref $self->formats eq 'HASH' + or carp 'formats must be a hash reference'; - # TODO: support file extensions and disabling formats + $self->via_param(1) unless defined $self->via_param; } sub call { @@ -114,7 +80,6 @@ sub call { unless defined $env->{'rdflight.uri'}; if ( $type ) { - # TODO: document this variables $env->{'rdflight.type'} = $type; $env->{'rdflight.serializer'} = $serializer; @@ -124,9 +89,13 @@ sub call { return [ 200, [ 'Content-Type' => $type ], [ $rdf_data ] ]; } } - - # pass through if no/unknown serializer or empty source (URI not found) or error - return $app->( $env ); + + # pass through if no/unknown serializer or empty source (URI not found) or error + if ( $app ) { + return $app->( $env ); + } else { + return [ 404, [ 'Content-Type' => 'text/plain' ], [ 'Not found' ] ]; + } } sub retrieve_and_serialize { @@ -146,20 +115,22 @@ sub retrieve_and_serialize { if ( UNIVERSAL::isa( $src, 'CODE' ) ) { $rdf = $src->($env); } elsif ( UNIVERSAL::isa( $src, 'RDF::Trine::Model' ) ) { - $rdf = $src->bounded_description( iri($env->{'rdflight.uri'}) ); + $rdf = $src->bounded_description( iri($env->{'rdflight.uri'}) ); + } elsif ( UNIVERSAL::can( $src, 'retrieve' ) ) { + $rdf = $src->retrieve( $env ); } if ( UNIVERSAL::isa( $rdf, 'RDF::Trine::Model' ) ) { - if ( $rdf->size > 0 ) { - return $serializer->serialize_model_to_string( $rdf ); - } + if ( $rdf->size > 0 ) { + return $serializer->serialize_model_to_string( $rdf ); + } } elsif ( UNIVERSAL::isa( $rdf, 'RDF::Trine::Iterator' ) ) { - if ( $rdf->peek ) { - return $serializer->serialize_iterator_to_string( $rdf ); - } + if ( $rdf->peek ) { + return $serializer->serialize_iterator_to_string( $rdf ); + } } else { - # TODO: how to indicate an error? (500?) - # $env->{'rdflight.error'} = ... ? + # TODO: how to indicate an error? (500?) + # $env->{'rdflight.error'} = ... ? } } @@ -181,15 +152,26 @@ sub guess_serialization { my $accept = $env->{HTTP_ACCEPT} || ''; my $req = Plack::Request->new( $env ); - my $format = $req->param('format') || ''; # TODO: also support extensions + my $format; + + if ($self->via_param and $req->param('format')) { + $format = $req->param('format'); + } elsif ($self->via_extension) { + my $path = $env->{PATH_INFO} || ''; + if ( $path =~ /^(.*)\.([^.]+)$/ and $possible_formats->{$2} ) { + $env->{PATH_INFO} = $1; + $format = $2; + } + } my ($type, $serializer); - if ($format ne '') { - try { - $serializer = RDF::Trine::Serializer->new( $possible_formats->{$format} ); + if ($format) { + my $name = $possible_formats->{$format}; + if ($name) { try { + $serializer = RDF::Trine::Serializer->new( $name ); ($type) = $serializer->media_types; - } # TODO: catch if unknown format or format not available + } } # TODO: catch if unknown format or format not available } else { ($type, $serializer) = try { RDF::Trine::Serializer->negotiate( request_headers => $req->headers ); @@ -209,7 +191,7 @@ sub uri { return $env->{'rdflight.uri'} if defined $env->{'rdflight.uri'}; - my ($base,$self); # TODO: support as second argument + my ($base, $self); # TODO: support as second argument if (UNIVERSAL::isa($env,'RDF::Light')) { ($self, $env) = ($env, shift); @@ -227,6 +209,40 @@ sub uri { return $base.$path; } +=head1 INTRODUCTION + +This package provides a PSGI application to serve RDF as Linked Data. In +contrast to other Linked Data applications, URIs must not have query parts and +the distinction between information-resources and non-information resources is +disregarded (some Semantic Web evangelists may be angry about this). By now +this package is experimental. For a more complete package see +L. + +The package implements a PSGI application that can be used as +L to provide RDF data. The implementation is based on +L which is a full implementation of RDF standards in Perl. + +=head1 OVERVIEW + +An RDF::Light application processes PSGI/HTTP requests in three steps: + +=over 4 + +=item 1 + +Determine query URI and serialization format (mime type) and set the request +variables C, C, and C. + +=item 2 + +Retrieve data about the resource which is identified by the request URI. + +=item 3 + +Create a serialization. + +=back + =head1 METHODS =head2 new ( [ %configuration ] ) @@ -239,15 +255,27 @@ Creates a new object. =item source -Sets a code reference as RDF source (see L) or a -L to query from. You can also set an array reference -with a list of multiple sources, which are cascaded. +Sets a L or a code reference as RDF source that returns a +Model or Iterator (see L) to query from. You can also set +an array reference with a list of multiple sources, which are cascaded. + +For testing you can use the function dummy_source that always returns a single +triple and is exported by RDF::Light::Source. + +=item base + +Maps request URIs to a given URI prefix, similar to L. + +For instance if you deploy you application at C and set +base to C then a request for C +is be mapped to the URI C. =item formats -Defines supported serialization formats. You can either specify an array reference -with serializer names or a hash reference with mappings of format names to serializer -names. Serializer names must exist in L::serializer_names. +Defines supported serialization formats. You can either specify an array +reference with serializer names or a hash reference with mappings of format +names to serializer names. Serializer names must exist in +RDF::Trine's L::serializer_names. RDF::Light->new ( formats => [qw(ntriples rdfxml turtle)] ) @@ -258,13 +286,22 @@ names. Serializer names must exist in L::serializer_name ttl => 'turtle' } ); -=item base +By default the formats rdf, xml, and rdfxml (for L), +ttl (for L), json +(for L), and nt +(for L) are used. -Maps request URIs to a given URI prefix, similar to L. +=item via_param -For instance if you deploy you application at C and set -base to C then a request for C -is be mapped to the URI C. +Detect serialization format via 'format' parameter. For instance +C will serialize URI foobar in RDF/Turtle. +This is enabled by default. + +=item via_extension + +Detect serialization format via "file extension". For instance +C will serialize URI foobar in RDF/XML. +This is disabled by default. =item extensions @@ -296,8 +333,7 @@ and path. Query parameters are ignored. =head2 SEE ALSO -See also L. -RDF::Light may be renamed to RDF::Light::LOD for "Linked Open Data". +See also L, which is bundled with this module. =head2 ACKNOWLEDGEMENTS diff --git a/lib/RDF/Light/Graph.pm b/lib/RDF/Light/Graph.pm index 83211b5..e4a67ed 100644 --- a/lib/RDF/Light/Graph.pm +++ b/lib/RDF/Light/Graph.pm @@ -1,7 +1,6 @@ -package RDF::Light::Graph; - use strict; use warnings; +package RDF::Light::Graph; =head1 NAME @@ -35,7 +34,7 @@ sub new { }, $class; } -sub model { shift->{model} } +sub model { $_[0]->{model} } sub objects { my $self = shift; @@ -46,7 +45,7 @@ sub objects { $subject = $self->node($subject) unless UNIVERSAL::isa( $subject, 'RDF::Light::Node' ); - my $all = 1 if ($property =~ s/^(.+[^_])_$/$1/); + my $all = ($property =~ s/^(.+[^_])_$/$1/) ? 1 : 0; my $predicate = $self->node($property); if (defined $predicate) { @@ -287,11 +286,11 @@ sub new { } sub uri { - shift->trine->value + shift->trine->uri_value } sub href { # TODO: check whether non-XML characters are possible - escapeHTML(shift->trine->value); + escapeHTML(shift->trine->uri_value); } sub objects { # TODO: rename to 'attr' or 'prop' ? diff --git a/lib/RDF/Light/Source.pm b/lib/RDF/Light/Source.pm index 6e22ea4..a621c64 100644 --- a/lib/RDF/Light/Source.pm +++ b/lib/RDF/Light/Source.pm @@ -2,11 +2,34 @@ use strict; use warnings; package RDF::Light::Source; -#use 5.010; - use Plack::Request; use RDF::Trine qw(iri statement); +use parent 'Exporter'; +use Carp; +our @EXPORT = qw(dummy_source); + +our $rdf_type = iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'); +our $rdfs_resource = iri('http://www.w3.org/2000/01/rdf-schema#Resource'); + +sub dummy_source { + my $env = shift; + my $uri = RDF::Light::uri( $env ); + + my $model = RDF::Trine::Model->temporary_model; + $model->add_statement( statement( iri($uri), $rdf_type, $rdfs_resource ) ); + + return $model; +} + +sub retrieve { + return RDF::Trine::Model->temporary_model; +} + +1; + +__END__ + =head1 DESCRIPTION A Source is a code reference that gets HTTP requests and returns RDF models. @@ -14,30 +37,40 @@ This is similar to L applications, which return HTTP responses. In contrast to a PSGI application, a source must return an object of type L or L. -In general you do not need to directly use this package. Just create a source -as code reference like one of the following examples: +In general you do not need to directly use this package. You can use any +of the following three as source: a code reference, an object that supports +the method 'retrieve', or an instance of RDF::Trine::Model: + + # 1. code reference my $source = sub { my $env = shift; - my $model = RDF::Trine::Model->temporary_model; - add_some_statements( $model ); - return $model; }; - my $source = sub { - my $env = shift; + # 2. object with 'retrieve' method (duck typing) - my $query = build_some_query_from( $env ); - my $iterator = query_model_for_some_triples( $model, $query ); + package MySource; + use parent 'Exporter'; + + sub retrieve { + my ($self, $env) = @_; + my $query = $self->build_query_from( $env ); + my $iterator = $self->get_triples_from( $query ); + return $iterator; # or model + } - return $model; - }; + # ...end of package, in your application just use: + + use MySource; + my $source = MySource->new( ... ); + + # 3. model instance -In addition you can use L as source which returns a -bounded description for a given request URI from that model. + my $model = RDF::Trine::Model->new( ... ); + my $source = $model; # RDF::Light will call $model->bounded_description This package contains the following function which is exported by default: @@ -52,22 +85,3 @@ request URI. The request URI is either taken from the PSGI request variable rdf:type rdfs:Resource . =cut - -use parent 'Exporter'; -use Carp; -our @EXPORT = qw(dummy_source); - -our $rdf_type = iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'); -our $rdfs_resource = iri('http://www.w3.org/2000/01/rdf-schema#Resource'); - -sub dummy_source { - my $env = shift; - my $uri = RDF::Light::uri( $env ); - - my $model = RDF::Trine::Model->temporary_model; - $model->add_statement( statement( iri($uri), $rdf_type, $rdfs_resource ) ); - - return $model; -} - -1; diff --git a/t/30_sources.t b/t/30_sources.t index 7a6014f..82c22d0 100644 --- a/t/30_sources.t +++ b/t/30_sources.t @@ -54,4 +54,24 @@ test_app code => 200, }]; +my $src = MySource->new; + +test_app + name => "Module as source", + app => RDF::Light->new( base => "http://example.com/", source => $src ), + tests => [{ + request => [ 'GET', '/foo', [ 'Accept' => 'text/turtle' ] ], + content => qr{foo> a.+Resource>}, + code => 200, + }]; + done_testing; + +package MySource; +use base 'RDF::Light::Source'; +use RDF::Light::Source; + +sub new { bless {}, shift; } +sub retrieve { dummy_source( $_[1] ) } + +1; \ No newline at end of file diff --git a/t/40_formats.t b/t/40_formats.t new file mode 100644 index 0000000..a38a41b --- /dev/null +++ b/t/40_formats.t @@ -0,0 +1,82 @@ +use strict; +use warnings; + +use lib 't'; +use TestPlackApp; + +use Test::More; +use RDF::Light; +use RDF::Light::Source; + +my $app = RDF::Light->new( + source => \&dummy_source +); + +test_app + app => $app, + tests => [{ + name => 'request format=ttl', + request => [ GET => '/example?format=ttl' ], + content => qr{example> a }, + headers => { 'Content-Type' => 'application/turtle' }, + }]; + +$app = RDF::Light->new( + source => \&dummy_source, + formats => { rdf => 'rdfxml' } +); + +test_app + name => 'selected formats', + app => $app, + tests => [{ + request => [ GET => '/example?format=rdf' ], code => 200 + },{ + request => [ GET => '/example?format=ttl' ], code => 404 + }]; + +$app = RDF::Light->new( + source => \&dummy_source, + via_param => 0, + via_extension => 1 +); + +test_app + name => 'format_extension', + app => $app, + tests => [{ + request => [ GET => '/example?format=ttl' ], code => 404 + },{ + request => [ GET => '/example.ttl' ], code => 200, + content => qr{example> a }, + },{ + request => [ GET => '/example.ttl.ttl' ], code => 200, + content => qr{example.ttl> a }, + },{ + request => [ GET => '/example.ttl?format=rdf' ], code => 200, + content => qr{example> a }, + }]; + + +$app = RDF::Light->new( + source => \&dummy_source, + via_param => 1, + via_extension => 1 +); + +test_app + name => 'format_extension', + app => $app, + tests => [{ + request => [ GET => '/example?format=ttl' ], code => 200, + headers => { 'Content-Type' => 'application/turtle' }, + },{ + request => [ GET => '/example.rdfxml' ], code => 200, + headers => { 'Content-Type' => 'application/rdf+xml' }, + },{ + request => [ GET => '/example.ttl?format=rdf' ], code => 200, + headers => { 'Content-Type' => 'application/rdf+xml' }, + }]; + + +done_testing; diff --git a/t/40_graph.t b/t/50_graph.t similarity index 100% rename from t/40_graph.t rename to t/50_graph.t diff --git a/t/TestPlackApp.pm b/t/TestPlackApp.pm index f6f2161..c92d30a 100644 --- a/t/TestPlackApp.pm +++ b/t/TestPlackApp.pm @@ -22,7 +22,7 @@ sub is_like { # run an array of tests with expected response on an app sub test_app { - my %arg = @_; + my %arg = ref($_[0]) ? (app => $_[0], tests => $_[1], name => $_[2]) : @_; my $app = $arg{app}; @@ -101,11 +101,24 @@ sub test_app { } }; - if ($arg{'name'}) { - subtest $arg{'name'} => $run; + if ($arg{name}) { + subtest $arg{name} => $run; } else { $run->(); } } 1; + +=head1 NAME + +TestPlackApp - Test PSGI applications with Plack::Test + +=head1 SEE ALSO + +L. + +This module is located at L until it is +merged into another Perl module or published as tested module. + +=cut diff --git a/t/template.t b/t/template.t index 8b32b16..fa2b6e0 100644 --- a/t/template.t +++ b/t/template.t @@ -10,6 +10,7 @@ use Data::Dumper; use Template; use RDF::Light::Graph; +use Carp; sub ttl_model { my $turtle = shift; @@ -43,8 +44,8 @@ $graph = RDF::Light::Graph->new( namespaces => $map, model => $model ); my $a = $graph->resource('http://example.com/"'); -test_tt('[% a %]', { a => $a }, 'http://example.com/"'); -test_tt('[% a.href %]', { a => $a }, 'http://example.com/"'); +test_tt('[% a %]', { a => $a }, 'http://example.com/"', 'plain URI with quot'); +test_tt('[% a.href %]', { a => $a }, 'http://example.com/"', 'escaped URI with quot'); $a = $graph->resource('http://example.org/alice'); $vars = { 'a' => $a };