@@ -36,6 +36,72 @@ local function convert_path(path_template)
3636end
3737
3838
39+ -- Detect path templates whose segments mix a {var} with literal text or
40+ -- multiple {var}s in the same segment, e.g. "/foo/{id}.json" or
41+ -- "/baz/{name}.{ext}". radixtree's :param syntax cannot extract these
42+ -- correctly because it consumes the entire `/`-bounded segment as one
43+ -- variable; the literal suffix is silently dropped from both the captured
44+ -- value and the param name. For such templates we fall back to a per-route
45+ -- PCRE that re-extracts path params at match time.
46+ local function has_mixed_segment (path_template )
47+ for seg in str_gmatch (path_template , " /([^/]*)" ) do
48+ local has_var = str_find (seg , " {" , 1 , true ) ~= nil
49+ if has_var then
50+ -- a clean segment is exactly "{name}" with nothing else
51+ if not (str_byte (seg , 1 ) == str_byte (" {" )
52+ and str_byte (seg , # seg ) == str_byte (" }" )
53+ and (str_find (seg , " }" , 2 , true ) == # seg )) then
54+ return true
55+ end
56+ end
57+ end
58+ return false
59+ end
60+
61+
62+ local PCRE_META = {
63+ [" %" ] = true , [" ." ] = true , [" *" ] = true , [" +" ] = true , [" ?" ] = true ,
64+ [" (" ] = true , [" )" ] = true , [" [" ] = true , [" ]" ] = true , [" {" ] = true ,
65+ [" }" ] = true , [" |" ] = true , [" ^" ] = true , [" $" ] = true , [" \\ " ] = true ,
66+ [" /" ] = true ,
67+ }
68+
69+ local function pcre_escape (s )
70+ return (str_gsub (s , " ." , function (c )
71+ if PCRE_META [c ] then return " \\ " .. c end
72+ end ))
73+ end
74+
75+
76+ -- Build a PCRE pattern + ordered name list that extracts path params from
77+ -- a request path matching `path_template`. Used as a fallback when
78+ -- has_mixed_segment(path_template) is true.
79+ local function build_param_pcre (path_template )
80+ local names = {}
81+ local out = {}
82+ local i = 1
83+ while i <= # path_template do
84+ local lb = str_find (path_template , " {" , i , true )
85+ if not lb then
86+ tab_insert (out , pcre_escape (sub_str (path_template , i )))
87+ break
88+ end
89+ if lb > i then
90+ tab_insert (out , pcre_escape (sub_str (path_template , i , lb - 1 )))
91+ end
92+ local rb = str_find (path_template , " }" , lb + 1 , true )
93+ if not rb then
94+ tab_insert (out , pcre_escape (sub_str (path_template , lb )))
95+ break
96+ end
97+ tab_insert (names , sub_str (path_template , lb + 1 , rb - 1 ))
98+ tab_insert (out , " ([^/]+?)" )
99+ i = rb + 1
100+ end
101+ return " ^" .. table.concat (out ) .. " $" , names
102+ end
103+
104+
39105-- Extract param names from {param} in path template.
40106local function extract_param_names (path_template )
41107 local names = {}
@@ -152,6 +218,11 @@ function _M.new(spec)
152218 local route_id = 0
153219 for path_template , path_item in pairs (paths ) do
154220 local param_names = extract_param_names (path_template )
221+ local mixed = has_mixed_segment (path_template )
222+ local param_pcre , pcre_names
223+ if mixed then
224+ param_pcre , pcre_names = build_param_pcre (path_template )
225+ end
155226
156227 for method , operation in pairs (path_item ) do
157228 local m = str_upper (method )
@@ -166,6 +237,9 @@ function _M.new(spec)
166237 route_metadata [id ] = {
167238 path_template = path_template ,
168239 param_names = param_names ,
240+ param_pcre = param_pcre ,
241+ pcre_names = pcre_names ,
242+ base_paths = base_paths ,
169243 method = m ,
170244 operation = operation ,
171245 params = params ,
@@ -240,6 +314,40 @@ function _M.match(self, method, path)
240314 path_params [name ] = matched [" _" .. name ]
241315 end
242316
317+ -- Fallback: when the template has mixed segments (e.g. "/foo/{id}.json"
318+ -- or "/baz/{name}.{ext}"), radixtree can match the route but cannot
319+ -- extract the variables (it consumes the whole `/`-bounded segment as
320+ -- one param and silently drops the literal suffix). Re-extract the
321+ -- params here using a per-route PCRE built from the template.
322+ if route .param_pcre then
323+ local re_match = ngx .re .match
324+ local m
325+ local bases = route .base_paths or { " " }
326+ for _ , base in ipairs (bases ) do
327+ local rel = path
328+ if base ~= " " then
329+ if str_find (path , base , 1 , true ) == 1 then
330+ rel = sub_str (path , # base + 1 )
331+ if rel == " " then rel = " /" end
332+ else
333+ rel = nil
334+ end
335+ end
336+ if rel then
337+ local mm = re_match (rel , route .param_pcre , " jo" )
338+ if mm then m = mm ; break end
339+ end
340+ end
341+ if not m then
342+ -- radixtree matched but our authoritative regex doesn't:
343+ -- treat as no match so callers get a clean error.
344+ return nil , nil
345+ end
346+ for i , name in ipairs (route .pcre_names or {}) do
347+ path_params [name ] = m [i ]
348+ end
349+ end
350+
243351 return route , path_params
244352end
245353
0 commit comments