From 4fd134b5a3eb9b4a22f4f155a0ff09ce8379aa2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Corr=C3=AAa=20de=20Oliveira?= Date: Sun, 28 Sep 2025 19:32:19 +0100 Subject: [PATCH] Adding named endpoints It adds `Str :$name` to the `http` functions (`get`, `put`, `post`, etc) and a `Str $.name` to `RouteHandler` class. On `Cro::HTTP::Router` it adds `Handler %.named-routes` and `Handler %.generated-named-routes` to store the handlers indexed by name (passed or generated). It also adds the methods `generate-name` to be able to generate a name if one wasnt provided, and `path-template` and `path` to, having the `Handler` (you can gat using the new exported sub `endpoint(Str $name)`) be able to get it's path-template and it's path (receiving named parameters to populate the vars). It also alters `redirect` to, if the location passed is a name of an endpoint, uses its path (it also accepts `:%params` to be used to populate the path) as location to be redirected (unless also passed `:!named-endpoint`). And finally it also exports a sub called `list-routes` that returns a string listing all routes and their names. --- Changes | 3 + lib/Cro/HTTP/Router.rakumod | 112 ++++++++++++++++++++++++++++++------ t/named-endpoints.rakutest | 79 +++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 t/named-endpoints.rakutest diff --git a/Changes b/Changes index 11df61f..8c7a0c9 100644 --- a/Changes +++ b/Changes @@ -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++ diff --git a/lib/Cro/HTTP/Router.rakumod b/lib/Cro/HTTP/Router.rakumod index 1162b0e..7471e0c 100644 --- a/lib/Cro/HTTP/Router.rakumod +++ b/lib/Cro/HTTP/Router.rakumod @@ -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) { @@ -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 } @@ -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))); } } @@ -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(); @@ -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; @@ -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 @@ -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); if $permanent { @@ -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; } @@ -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 diff --git a/t/named-endpoints.rakutest b/t/named-endpoints.rakutest new file mode 100644 index 0000000..bbc3d41 --- /dev/null +++ b/t/named-endpoints.rakutest @@ -0,0 +1,79 @@ +use Cro::HTTP::Router; +use Test; +use lib 't/TestModule'; +use TestModule; + +my $app = route { + get :name, -> Int $number { + content 'text/plain', "Int: $number"; + } + post :name, -> "bla", Str $string { + redirect "ep1", :params{ :number($string.Int) }; + } + get :name, -> "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, ; +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, '/'; +is endpoint("ep2").path-template, '/bla/'; + +is list-routes, Q:to/END/.chomp; +/: + - GET - GET_Cro::HTTP::Auth + - POST - POST_Cro::HTTP::Auth +/: + - GET - ep1 +/bla/: + - 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, :target); +$source.emit($req); +is $responses.receive.header('Location'), "/42", "location"; + +$req = Cro::HTTP::Request.new(:method, :target); +$source.emit($req); +is $responses.receive.header('Location'), "/13", "location"; + +done-testing;