From 8a4306b12c1d758cb1adf4414f5799fff849533a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2026 16:06:10 -0800 Subject: [PATCH] Adding to the route helpers so we can build route objects with globs. Fixes #1926 --- spec/lucky/action_route_params_spec.cr | 51 +++++++++++++++++++++++ src/lucky/routable.cr | 56 ++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/spec/lucky/action_route_params_spec.cr b/spec/lucky/action_route_params_spec.cr index a2889d046..ac39f4411 100644 --- a/spec/lucky/action_route_params_spec.cr +++ b/spec/lucky/action_route_params_spec.cr @@ -64,3 +64,54 @@ describe "Automatically generated param helpers" do typeof(action.leftover).should eq String? end end + +describe "Glob route URL building" do + it "builds URL with unnamed glob param using .with" do + TestGlobAction.with(glob: "some/path").path.should eq "/test_complex_posts_glob/some/path" + end + + it "builds URL with named glob param using .with" do + TestNamedGlobAction.with(leftover: "some/path").path.should eq "/test_complex_posts_named_glob/some/path" + end + + it "builds URL without glob param" do + TestGlobAction.with.path.should eq "/test_complex_posts_glob" + TestNamedGlobAction.with.path.should eq "/test_complex_posts_named_glob" + end + + it "builds URL with nil glob param" do + TestGlobAction.with(glob: nil).path.should eq "/test_complex_posts_glob" + TestNamedGlobAction.with(leftover: nil).path.should eq "/test_complex_posts_named_glob" + end + + it "URL-encodes special characters in glob segments" do + TestGlobAction.with(glob: "path with spaces/and more").path.should eq "/test_complex_posts_glob/path+with+spaces/and+more" + end + + it "normalizes consecutive slashes in glob value" do + TestGlobAction.with(glob: "a//b").path.should eq "/test_complex_posts_glob/a/b" + end + + it "strips leading slashes from glob value" do + TestGlobAction.with(glob: "/leading/slash").path.should eq "/test_complex_posts_glob/leading/slash" + end + + it "builds URL with glob using .route" do + TestNamedGlobAction.route(leftover: "some/path").path.should eq "/test_complex_posts_named_glob/some/path" + end + + it "builds path_without_query_params with glob" do + TestNamedGlobAction.path_without_query_params(leftover: "some/path").should eq "/test_complex_posts_named_glob/some/path" + end + + it "builds url_without_query_params with glob" do + Lucky::RouteHelper.temp_config(base_uri: "example.com") do + TestNamedGlobAction.url_without_query_params(leftover: "some/path").should eq "example.com/test_complex_posts_named_glob/some/path" + end + end + + it "returns correct RouteHelper with glob" do + route_helper = TestGlobAction.with(glob: "a/b/c") + route_helper.should eq Lucky::RouteHelper.new(:get, "/test_complex_posts_glob/a/b/c") + end +end diff --git a/src/lucky/routable.cr b/src/lucky/routable.cr index 5c5db85e7..0d128980b 100644 --- a/src/lucky/routable.cr +++ b/src/lucky/routable.cr @@ -225,6 +225,16 @@ module Lucky::Routable end {% end %} + # Extract glob param name for use in URL building methods + {% glob_param_name = nil %} + {% if glob_param %} + {% if glob_param.starts_with?("*:") %} + {% glob_param_name = glob_param.gsub(/\*:/, "") %} + {% elsif glob_param == "*" %} + {% glob_param_name = "glob" %} + {% end %} + {% end %} + def self.path(*args, **named_args) : String route(*args, **named_args).path end @@ -240,6 +250,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }} = nil, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }} = nil, + {% end %} subdomain : String? = nil ) : String path = path_from_parts( @@ -249,6 +262,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) Lucky::RouteHelper.new({{ method }}, path, subdomain).url end @@ -260,6 +276,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }} = nil, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }} = nil, + {% end %} subdomain : String? = nil ) : String path = path_from_parts( @@ -269,6 +288,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) Lucky::RouteHelper.new({{ method }}, path, subdomain).path end @@ -300,6 +322,11 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }} = nil, {% end %} + + # glob param is optional + {% if glob_param_name %} + {{ glob_param_name.id }} = nil, + {% end %} anchor : String? = nil, subdomain : String? = nil ) : Lucky::RouteHelper @@ -312,6 +339,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) query_params = URI::Params.build do |builder| @@ -363,6 +393,11 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }} = nil, {% end %} + + # glob param is optional + {% if glob_param_name %} + {{ glob_param_name.id }} = nil, + {% end %} anchor : String? = nil, subdomain : String? = nil ) : Lucky::RouteHelper @@ -383,6 +418,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) : Nil {% for part in path_parts %} {% if part.starts_with?("?:") %} @@ -393,11 +431,23 @@ module Lucky::Routable {% elsif part.starts_with?(':') %} io << '/' URI.encode_www_form({{ part.gsub(/:/, "").id }}.to_param, io) + {% elsif part.starts_with?("*") %} + # glob param handled separately below {% else %} io << '/' URI.encode_www_form({{ part }}, io) {% end %} {% end %} + {% if glob_param_name %} + if _glob_value = {{ glob_param_name.id }} + _glob_value.to_param.split('/').each do |segment| + unless segment.empty? + io << '/' + URI.encode_www_form(segment, io) + end + end + end + {% end %} end private def self.path_from_parts( @@ -407,6 +457,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) : String path = String.build do |io| path_from_parts( @@ -417,6 +470,9 @@ module Lucky::Routable {% for param in optional_path_params %} {{ param.gsub(/^\?:/, "").id }}, {% end %} + {% if glob_param_name %} + {{ glob_param_name.id }}, + {% end %} ) end