=head1 NAME WWW::Kontent::Path - Classes for navigating Kontent stores =head1 SYNOPSIS my $path = WWW::Kontent::Path.new; $path.parse('foo/bar[42]/baz.pdf{view}'); $path.resolve(:in($root), :request($r)); say $path.components[0].name; # foo say $path.components[1].revno; # 42 say ref $path.components[3].revision; # WWW::Kontent::Store::NarrowDBI::SavedRev say $path.format; # pdf =head1 DESCRIPTION WWW::Kontent::Path is a class representing a path to a Kontent page. Besides the Path object itself, it also defines WWW::Kontent::Component, a single part of a path. =head2 Paths in Kontent A path is the portion of the URL which Kontent examines to determine which page it should operate on, which mode that page should be processed in, and which format it should be rendered in. Expressed as a Perl 6 grammar, it might look something like this: grammar Grammar::Kontent::Path { rule name { \w+ } rule revno { \[ \d+ \] } rule format { \. } rule mode { \{ \} } rule component { ? } rule path { /? [ | ] [ / ]* ? ? } } In somewhat simpler terms, all of these are valid: // []/{mode} []//[]. As well as most similar combinations. =head2 WWW::Kontent::Component Component objects contain two accessors, C and C, representing the name and revision number of that particular part of the path. C is undefined in the first component, representing the root node; if C is undefined it means that the current revision should be used. After C has been called on the WWW::Kontent::Path object it belongs to, two additional fields become available. C and C contain the page and revision, respectively, associated with the component. A component can be stringified, yielding a string something like C, but intelligent enough to omit missing parts of the component. Component objects should never be allocated by user code; only WWW::Kontent::Path should create them. =cut class WWW::Kontent::Component is rw { has Str $.name; has Int $.revno; has WWW::Kontent::Page $.page; has WWW::Kontent::Revision $.revision; method resolve_in(WWW::Kontent::SavedRevision $in, WWW::Kontent::Request $request) returns WWW::Kontent::Revision { $request.trigger_magic('pre', 'resolve', $_, $in); if $in { $.page = $in.resolve($.name, $request); } if $.revno { $.revision = $.page.revisions[$.revno]; } else { $.revision = $.page.cur; } $.revision = $request.trigger_magic('post', 'resolve', $_, $in, $.revision); $.page = $.revision.page; return $.revision; } method prefix:<~> () returns Str { my $str = $.name; $str ~= "[$.revno]" if $.revno; return $str; } } =head2 WWW::Kontent::Path Represents a full path (set of components). Once a Path object has been allocated, a string path must be given to the C method; later, a call to C (with the root node and request passed in) will find the pages and revisions associated with those path components. The C accessor, filled in by C, contains an array of Component objects. The C accessor contains the mode, while the C accessor contains the format. The C and C accessors retrieve the page and revision, respectively, of the last component, and are only useful after C has been called. =cut class WWW::Kontent::Path { has WWW::Kontent::Component @.components is rw; method page() { @.components[-1].page } method revision() { @.components[-1].revision } has Str $.mode is rw; has Str $.format is rw; method parse(Str $path is copy) { # Extract the mode and format first. if $path ~~ s/ \{ (\w+) \} $// { $.mode = ~$0; } else { $.mode = 'view'; } if $path ~~ s/ \. (\w+) $// { $.format = ~$0; } else { $.format = 'html'; } @.components = gather { # Strip leading or trailing slash, if any. $path ~~ s:g{^/|/$}{}; # A leading [NUM] with no name indicates the root revision to use. if $path ~~ s{^ \[ (\d+) \] /?}{} { take WWW::Kontent::Component.new(:revno(+$0)); } else { take WWW::Kontent::Component.new(); } #Parse the rest of the path if $path { for split '/', $path -> $comp { $comp ~~ /^ (\w+) [ \[ (\d+) \] ]? $/ or WWW::Kontent::error("Invalid component '$comp'", :code(400)); take WWW::Kontent::Component.new(:name(~$0), :revno(+$1)); } } }; return $_; } method resolve(WWW::Kontent::SavedPage $in, WWW::Kontent::Request $request) returns WWW::Kontent::SavedRevision { WWW::Kontent::error("Can't resolve a path which hasn't been parsed yet!") unless @.components; @.components[0].page = $in; my $cur; $cur = $_.resolve_in($cur, $request) for @.components; return $cur; } method stringify () returns Str { return join '/', grep { $_.chars } ~<< @.components; } } =head1 SEE ALSO L =cut