PathMatch is a Go library for flexible matching of URL-like paths against defined templates. It allows for extracting named variables from paths and supports wildcards for more complex matching rules.
The library is useful for routing, resource identification, or any scenario where structured path analysis is required. For instance, given a template /users/{userID}/data
and an input path /users/123/data
, PathMatch can confirm the match and extract userID="123"
.
It also provides a Walker
type for step-by-step path consumption against a sequence of templates.
- Match concrete paths against templates with literals, wildcards (
*
,**
), and named variables. - Extract variables from matched paths.
- Support for sub-templates in variables, allowing multi-segment captures.
- Step-by-step path matching for hierarchical or multi-stage scenarios.
- Templates are parsed into protocol buffer (proto) messages and can be stored or reused efficiently.
go get github.com/tsdkv/pathmatch
The core operation involves matching a concrete path (e.g., /users/123/profile
) against a parsed PathTemplate
. A successful match confirms that the path conforms to the template's structure and allows for the extraction of any defined variables.
templatePattern := "/users/{userID}/posts/{postID}"
path := "/users/alice/posts/123"
matched, vars, err := pathmatch.CompileAndMatch(templatePattern, path)
// matched == true
// vars == map[string]string{"userID": "alice", "postID": "123"}
templatePattern := "/items/{category}/{itemID=**}"
tmpl, _ := pathmatch.ParseTemplate(templatePattern)
matched, vars, err := pathmatch.Match(tmpl, "/items/electronics/tv/samsung/qled80")
// matched == true
// vars == map[string]string{"category": "electronics", "itemID": "/tv/samsung/qled80"}
The Walker
type allows for a more controlled, step-by-step traversal of a concrete path. You initialize a Walker
with a concrete path and then use its Step
method with different PathTemplate
s to consume the path segment by segment. This is useful for navigating hierarchical structures or applying a sequence of rules.
walker := pathmatch.NewWalker("/users/alice/settings/profile/view")
// Initial state
// walker.Depth() == 0
// walker.Remaining() == "/users/alice/settings/profile/view"
// walker.Variables() == map[string]string{}
userTemplate, _ := pathmatch.ParseTemplate("/users/{userID}")
stepVars, ok, _ := walker.Step(userTemplate)
// stepVars == map[string]string{"userID": "alice"}, ok == true
// walker.Depth() == 1
// walker.Remaining() == "/settings/profile/view"
// walker.Variables() == map[string]string{"userID": "alice"}
settingsTemplate, _ := pathmatch.ParseTemplate("/settings/{section}")
stepVars, ok, _ = walker.Step(settingsTemplate)
// stepVars == map[string]string{"section": "profile"}, ok == true
// walker.Depth() == 2
// walker.Remaining() == "/view" (assuming /settings/{section} matched /settings/profile)
// walker.Variables() == map[string]string{"userID": "alice", "section": "profile"}
// Step back
steppedBack := walker.StepBack() // true
// walker.Depth() == 1
// walker.Remaining() == "/settings/profile/view"
// walker.Variables() == map[string]string{"userID": "alice"}
// Try to match another template
actionTemplate, _ := pathmatch.ParseTemplate("/settings/profile/{action}")
stepVars, ok, _ = walker.Step(actionTemplate)
// stepVars == map[string]string{"action": "view"}, ok == true
// walker.Depth() == 2
// walker.Remaining() == ""
// walker.Variables() == map[string]string{"userID": "alice", "action": "view"}
walker.IsComplete() // true
// Reset the walker
walker.Reset()
// walker.Depth() == 0
// walker.Remaining() == "/users/alice/settings/profile/view"
// walker.Variables() == map[string]string{}
Templates must start with a /
. Path segments are separated by /
.
-
Literals:
- Exact string matches for a path segment (e.g.,
users
,config
). - Can contain any character except
/
,*
,{
,}
.
- Exact string matches for a path segment (e.g.,
-
Variables:
- Format:
{variableName}
- Acts as a placeholder for a single, dynamic path segment.
- Example:
/users/{userID}
matches/users/alice
and capturesuserID="alice"
. - Variable names must follow the same rules as literals (no
/
,*
,{
,}
).
- Format:
-
Single-Segment Wildcard (
*
):- Matches exactly one path segment.
- Example:
/files/*/details
matches/files/image.png/details
and/files/document.pdf/details
. - The value matched by
*
is not captured as a named variable.
-
Multi-Segment Wildcard (
\*\*
):- Matches zero or more consecutive path segments.
- Constraint: Can only appear as the last segment of a path template.
- Example:
/data/**
matches/data
,/data/foo
, and/data/foo/bar/baz
. - Invalid:
/data/**/config
.
- Example:
- The value matched by
**
is not captured as a named variable.
-
Variables with Sub-Templates:
- Syntax:
{variableName=pattern}
. - The
pattern
is a sequence of one or more segments, separated by/
, and can include literals,*
, or a single**
at the end. - Example:
/files/{path=**}
matches/files/a/b/c
and capturespath="a/b/c"
. - Limitations:
pattern
cannot be empty.- Nested variables are not allowed (e.g.,
{var={subvar}}
is invalid). - If
**
appears in the pattern, it must be the last segment of the entire template.- Example:
/files/{rest=**}
is valid. - Example:
/files/{rest=**}/extra
is invalid.
- Example:
- Syntax:
- Optional segments (e.g.,
/foo/bar?/baz
,/foo/{var?}/baz
). - Options like case sensitivity, strict matching, etc.
- Add custom types for variables, for example:
/users/{id:int}/profile/{section:string}
/users/{id:uuid}/profile
- Support regex patterns for variable matching, e.g.,
{id:[0-9]+}
. - Implement more complex sub-template matching, such as allowing
**
in the middle of a sub-template (e.g.,{var=prefix/**/suffix}
). - Fuzz testing to ensure robustness against malformed paths and templates.
- Secutiry features, such as escaping or sanitizing paths to prevent injection attacks.
Contributions are welcome! Please feel free to submit issues or pull requests.
This project is licensed under the MIT License. See the LICENSE file for details.