Skip to content

Commit

Permalink
feat(router/atc): add new field http.path.segments.len (#12398)
Browse files Browse the repository at this point in the history
This field represents how much segments the request path contains.

For example, "/a/b/c/" contains 3 segments, "/a" contains 1 and "/" contains 0 segment.

It is useful for implementing segment based routing logic such as these used in OpenAPI spec.

KAG-3604
  • Loading branch information
chronolaw authored and ADD-SP committed Mar 7, 2024
1 parent 073cb56 commit bfae925
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 14 deletions.
7 changes: 4 additions & 3 deletions kong/db/schema/entities/routes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ do

for _, f in ipairs(fields) do
if f:find(HTTP_PATH_SEGMENTS_PREFIX, 1, true) then
local m = re_match(f:sub(#HTTP_PATH_SEGMENTS_PREFIX + 1),
HTTP_PATH_SEGMENTS_SUFFIX_REG, "jo")
local suffix = f:sub(#HTTP_PATH_SEGMENTS_PREFIX + 1)
local m = re_match(suffix, HTTP_PATH_SEGMENTS_SUFFIX_REG, "jo")

if not m or (m[2] and tonumber(m[1]) >= tonumber(m[3])) then
if (suffix ~= "len") and
(not m or (m[2] and tonumber(m[1]) >= tonumber(m[3]))) then
return nil, "Router Expression failed validation: " ..
"illformed http.path.segments.* field"
end
Expand Down
32 changes: 25 additions & 7 deletions kong/router/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ local HTTP_FIELDS = {
},

["Int"] = {"net.src.port", "net.dst.port",
"http.path.segments.len",
},

["IpAddr"] = {"net.src.ip", "net.dst.ip",
Expand Down Expand Up @@ -209,10 +210,32 @@ if is_http then

local HTTP_SEGMENTS_PREFIX = "http.path.segments."
local HTTP_SEGMENTS_PREFIX_LEN = #HTTP_SEGMENTS_PREFIX
local HTTP_SEGMENTS_REG_CTX = { pos = 2, } -- skip first '/'
local HTTP_SEGMENTS_OFFSET = 1


local get_http_segments
do
local HTTP_SEGMENTS_REG_CTX = { pos = 2, } -- skip first '/'

get_http_segments = function(params)
if not params.segments then
HTTP_SEGMENTS_REG_CTX.pos = 2 -- reset ctx, skip first '/'
params.segments = re_split(params.uri, "/", "jo", HTTP_SEGMENTS_REG_CTX)
end

return params.segments
end
end


FIELDS_FUNCS["http.path.segments.len"] =
function(params)
local segments = get_http_segments(params)

return #segments
end


-- func => get_headers or get_uri_args
-- name => "headers" or "queries"
-- max_config_option => "lua_max_req_headers" or "lua_max_uri_args"
Expand Down Expand Up @@ -281,12 +304,7 @@ if is_http then
local range = field:sub(HTTP_SEGMENTS_PREFIX_LEN + 1)

f = function(params)
if not params.segments then
HTTP_SEGMENTS_REG_CTX.pos = 2 -- reset ctx, skip first '/'
params.segments = re_split(params.uri, "/", "jo", HTTP_SEGMENTS_REG_CTX)
end

local segments = params.segments
local segments = get_http_segments(params)

local value = segments[range]

Expand Down
24 changes: 20 additions & 4 deletions spec/01-unit/01-db/01-schema/06-routes_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1563,16 +1563,28 @@ describe("routes schema (flavor = expressions)", function()
end)

it("http route supports http.path.segments.* fields", function()
local route = {
local r = {
id = a_valid_uuid,
name = "my_route",
protocols = { "grpcs" },
expression = [[http.path.segments.0 == "foo" && http.path.segments.1 ^= "bar" && http.path.segments.20_30 ~ r#"x/y"#]],
priority = 100,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
assert.truthy(Routes:validate(route))

local expressions = {
[[http.path.segments.0 == "foo"]],
[[http.path.segments.1 ^= "bar"]],
[[http.path.segments.20_30 ~ r#"x/y"#]],
[[http.path.segments.len == 10]],
}

for _, exp in ipairs(expressions) do
r.expression = exp

local route = Routes:process_auto_fields(r, "insert")
assert.truthy(Routes:validate(route))
end

end)

it("fails if http route has invalid http.path.segments.* fields", function()
Expand All @@ -1585,6 +1597,10 @@ describe("routes schema (flavor = expressions)", function()
}

local wrong_expressions = {
[[http.path.segments.len0 == 10]],
[[http.path.segments.len_a == 10]],
[[http.path.segments.len == "10"]],

[[http.path.segments. == "foo"]],
[[http.path.segments.abc == "foo"]],
[[http.path.segments.a_c == "foo"]],
Expand Down
42 changes: 42 additions & 0 deletions spec/01-unit/08-router_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5441,6 +5441,32 @@ do
assert.falsy(match_t)
end)

it("select() should match http.segments.* with len", function()
local use_case = {
{
service = service,
route = {
id = "e8fb37f1-102d-461e-9c51-6608a6bb8101",
expression = [[http.path.segments.0 == "foo" && http.path.segments.len == 1]],
priority = 100,
},
},
}

local router = assert(new_router(use_case))

local match_t = router:select("GET", "/foo")
assert.truthy(match_t)
assert.same(use_case[1].route, match_t.route)

local match_t = router:select("GET", "/foo/")
assert.truthy(match_t)
assert.same(use_case[1].route, match_t.route)

local match_t = router:select("GET", "/foo/xxx")
assert.falsy(match_t)
end)

it("select() should match range http.segments.*", function()
local use_case = {
{
Expand All @@ -5459,6 +5485,14 @@ do
priority = 100,
},
},
{
service = service,
route = {
id = "e8fb37f1-102d-461e-9c51-6608a6bb8103",
expression = [[http.path.segments.1_2 == r#"xxx/yyy"# && http.path.segments.len == 3]],
priority = 100,
},
},
}

local router = assert(new_router(use_case))
Expand All @@ -5474,6 +5508,14 @@ do
local match_t = router:select("GET", "/foo/xxx/yyy/zzz/bar")
assert.truthy(match_t)
assert.same(use_case[2].route, match_t.route)

local match_t = router:select("GET", "/foo/xxx/yyy")
assert.truthy(match_t)
assert.same(use_case[3].route, match_t.route)

local match_t = router:select("GET", "/foo/xxx/yyy/")
assert.truthy(match_t)
assert.same(use_case[3].route, match_t.route)
end)

it("select() accepts but does not match wrong http.segments.*", function()
Expand Down

0 comments on commit bfae925

Please sign in to comment.