Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Revision history for Cro::HTTP

{{NEXT}}
- Add named endpoints

0.8.11
- Avoid sending a 0-byte WINDOW_UPDATE frame.
- Permit use of updated HTTP::Pack module, Samuel Gillespie++
Expand Down
112 changes: 95 additions & 17 deletions lib/Cro/HTTP/Router.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,48 @@ module Cro::HTTP::Router {
has &.implementation;
has Hash[Array, Cro::HTTP::Router::PluginKey] $.plugin-config;
has Hash[Array, Cro::HTTP::Router::PluginKey] $.flattened-plugin-config;
has Str $.name;

method generate-name {
my @params = $.signature.params.grep: { !.named };
@params .= map: {
when .constraint_list.elems == 1 && !.?name {
.constraint_list.head.Str
}
default { .type.^name }
}
@params.join: "_"
}

method path-template {
my @params = $.signature.params.grep: { !.named };
@params .= map: {
when .constraint_list.elems == 1 && !.?name {
.constraint_list.head.Str
}
when .?type ~~ Any {
"<{ .type.^name } { .name }>"
}
default { "<{ .name }>" }
}
"/" ~ @params.join: "/"
}

method path(*%values) {
my @params = $.signature.params.grep: { !.named };
@params .= map: {
when .constraint_list.elems == 1 {
.constraint_list.head
}
default {
my $attr = $_;
my $val = %values{ $attr.name.substr: 1 };
die "Value '{ $val }' for '{ $attr.raku }' does not match" unless $val ~~ $attr.type && $val ~~ $attr.constraints;
$val.Str
}
}
"/" ~ @params.join: "/"
}

method copy-adding(:@prefix, :@body-parsers!, :@body-serializers!, :@before-matched!, :@after-matched!, :@around!,
Hash[Array, Cro::HTTP::Router::PluginKey] :$plugin-config) {
Expand Down Expand Up @@ -292,6 +334,8 @@ module Cro::HTTP::Router {
has $!path-matcher;
has @!handlers-to-add; # Closures to defer adding, so they get all the middleware
has Array %!plugin-config{Cro::HTTP::Router::PluginKey};
has Handler %.named-routes;
has Handler %.generated-named-routes;

method consumes() { Cro::HTTP::Request }
method produces() { Cro::HTTP::Response }
Expand Down Expand Up @@ -353,10 +397,10 @@ module Cro::HTTP::Router {
}
}

method add-handler(Str $method, &implementation --> Nil) {
method add-handler(Str $method, &implementation, Str :$name --> Nil) {
@!handlers-to-add.push: {
@!handlers.push(RouteHandler.new(:$method, :&implementation, :@!before-matched, :@!after-matched,
:@!around, :%!plugin-config));
:@!around, :%!plugin-config, |(:$name with $name)));
}
}

Expand Down Expand Up @@ -416,11 +460,22 @@ module Cro::HTTP::Router {
for @!handlers {
.body-parsers = @!body-parsers;
.body-serializers = @!body-serializers;
with .?name -> $name {
%!named-routes{$name} = $_;
}
%!generated-named-routes{"{ .method.uc }_{ .generate-name }"} = $_ if .^can: "method";
}
for @!includes -> (:@prefix, :$includee) {
for $includee.handlers() {
@!handlers.push: .copy-adding(:@prefix, :@!body-parsers, :@!body-serializers,
:@!before-matched, :@!after-matched, :@!around, :%!plugin-config);
with .?name -> $name {
%.named-routes{$name} = $_;
}
if .^can: "method" {
%!generated-named-routes{"{ .method.uc }_{ .generate-name }"} = $_;
%!generated-named-routes{[.method.uc, |@prefix, .generate-name].join: "_"} = $_;
}
}
}
self!generate-route-matcher();
Expand Down Expand Up @@ -688,10 +743,30 @@ module Cro::HTTP::Router {
}
}

sub endpoint($name, $routes = $*ROUTE-ROOT) is export {
$routes.named-routes{$name} // $routes.generated-named-routes{$name}
}

sub list-routes($routes = $*ROUTE-ROOT) is export {
my %paths = $routes.handlers.classify: {("/" ~ .prefix.join("/") if .prefix) ~ .path-template};

%paths.keys.sort.map(-> Str $path {
my @handlers := %paths{$path};
(
"{ $path }:",
@handlers.map({
"- { .method.uc.fmt("% -6s") }{ " - { .name // .method ~ "_" ~ .generate-name }" }"
}).join("\n").indent: 4
).join: "\n"
}).join: "\n"

}

#| Define a set of routes. Expects to receive a block, which will be evaluated
#| to set up the routing definition.
sub route(&route-definition) is export {
my $*CRO-ROUTE-SET = RouteSet.new;
PROCESS::<$ROUTE-ROOT> //= $*CRO-ROUTE-SET;
route-definition();
$*CRO-ROUTE-SET.definition-complete();
my @before = $*CRO-ROUTE-SET.before;
Expand All @@ -705,32 +780,32 @@ module Cro::HTTP::Router {

#| Add a handler for a HTTP GET request. The signature of the handler will be
#| used to determine the routing.
multi get(&handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('GET', &handler);
multi get(&handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('GET', &handler, |(:$name with $name));
}

#| Add a handler for a HTTP POST request. The signature of the handler will be
#| used to determine the routing.
multi post(&handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('POST', &handler);
multi post(&handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('POST', &handler, |(:$name with $name));
}

#| Add a handler for a HTTP PUT request. The signature of the handler will be
#| used to determine the routing.
multi put(&handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('PUT', &handler);
multi put(&handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('PUT', &handler, |(:$name with $name));
}

#| Add a handler for a HTTP DELETE request. The signature of the handler will be
#| used to determine the routing.
multi delete(&handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('DELETE', &handler);
multi delete(&handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('DELETE', &handler, |(:$name with $name));
}

#| Add a handler for a HTTP PATCH request. The signature of the handler will be
#| used to determine the routing.
multi patch(&handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('PATCH', &handler);
multi patch(&handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler('PATCH', &handler, |(:$name with $name));
}

#| Add a body parser, which will be considered for use when parsing the body of
Expand Down Expand Up @@ -972,7 +1047,10 @@ module Cro::HTTP::Router {

#| Produce a HTTP redirect response, defaulting to a temporary redirect (HTTP 307).
#| The location is the address to redirect to.
multi redirect(Str() $location, :$temporary, :$permanent, :$see-other --> Nil) {
multi redirect(Str() $location is copy, :%params, :$temporary, :$permanent, :$see-other, :$named-endpoint = True --> Nil) {
if $named-endpoint {
$location = .path: |%params with endpoint $location;
}
my $resp = $*CRO-ROUTER-RESPONSE //
die X::Cro::HTTP::Router::OnlyInHandler.new(:what<redirected>);
if $permanent {
Expand All @@ -991,9 +1069,9 @@ module Cro::HTTP::Router {
#| The location is the address to redirect to. The remaining arguments will be
#| passed to the content function, setting the media type, response body, and
#| other options.
multi redirect(Str() $location, $content-type, $body, :$temporary,
multi redirect(Str() $location, $content-type, $body, :%params, :$temporary,
:$permanent, :$see-other, *%options --> Nil) {
redirect $location, :$permanent, :$see-other;
redirect $location, :%params, :$permanent, :$see-other;
content $content-type, $body, |%options;
}

Expand Down Expand Up @@ -1284,8 +1362,8 @@ module Cro::HTTP::Router {

#| Add a request handler for the specified HTTP method. This is useful
#| when there is no shortcut function available for the HTTP method.
sub http($method, &handler --> Nil) is export {
$*CRO-ROUTE-SET.add-handler($method, &handler);
sub http($method, &handler, Str :$name --> Nil) is export {
$*CRO-ROUTE-SET.add-handler($method, &handler, |(:$name with $name));
}

#| Set a cache control header on the response according to the provided
Expand Down
79 changes: 79 additions & 0 deletions t/named-endpoints.rakutest
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use Cro::HTTP::Router;
use Test;
use lib 't/TestModule';
use TestModule;

my $app = route {
get :name<ep1>, -> Int $number {
content 'text/plain', "Int: $number";
}
post :name<ep2>, -> "bla", Str $string {
redirect "ep1", :params{ :number($string.Int) };
}
get :name<ep3>, -> "bla", "ble", "bli" {
content 'text/plain', "OK"
}
get -> Cro::HTTP::Auth $session {
content 'text/plain', 'You are ' ~ $session.username;
}
post -> Cro::HTTP::Auth $session {
content 'text/plain', "POST";
}

include test => resourcey-routes();
}

is-deeply $*ROUTE-ROOT, $app;

is-deeply $app.named-routes.keys.sort, <ep1 ep2 ep3>;
is-deeply $app.generated-named-routes.keys.sort, <
GET_Cro::HTTP::Auth GET_Int GET_bla_ble_bli
GET_folder-indexes GET_index.html GET_root-indexes1
GET_root-indexes2 GET_test-plugin GET_test.1
GET_test.2 GET_test_folder-indexes GET_test_index.html
GET_test_root-indexes1 GET_test_root-indexes2
GET_test_test-plugin GET_test_test.1 GET_test_test.2
POST_Cro::HTTP::Auth POST_bla_Str
>;

is endpoint("ep1").path-template, '/<Int $number>';
is endpoint("ep2").path-template, '/bla/<Str $string>';

is list-routes, Q:to/END/.chomp;
/<Cro::HTTP::Auth $session>:
- GET - GET_Cro::HTTP::Auth
- POST - POST_Cro::HTTP::Auth
/<Int $number>:
- GET - ep1
/bla/<Str $string>:
- POST - ep2
/bla/ble/bli:
- GET - ep3
/test/folder-indexes:
- GET - GET_folder-indexes
/test/index.html:
- GET - GET_index.html
/test/root-indexes1:
- GET - GET_root-indexes1
/test/root-indexes2:
- GET - GET_root-indexes2
/test/test-plugin:
- GET - GET_test-plugin
/test/test.1:
- GET - GET_test.1
/test/test.2:
- GET - GET_test.2
END

my $source = Supplier.new;
my $responses = $app.transformer($source.Supply).Channel;

my $req = Cro::HTTP::Request.new(:method<POST>, :target</bla/42>);
$source.emit($req);
is $responses.receive.header('Location'), "/42", "location";

$req = Cro::HTTP::Request.new(:method<POST>, :target</bla/13>);
$source.emit($req);
is $responses.receive.header('Location'), "/13", "location";

done-testing;