pathmatch

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2025 License: MIT Imports: 5 Imported by: 0

README

pathmatch

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.

What does PathMatch do?

At its core, PathMatch checks if a given path (like a URL) conforms to a predefined template and extracts any dynamic values from it.

Imagine you have a template for user profiles in your web application:

Template: /users/{userID}/profile

PathMatch can then process incoming paths against this template:

Incoming Path Does it Match? Extracted Variables
/users/alice/profile ✅ Yes userID = "alice"
/users/12345/profile ✅ Yes userID = "12345"
/users/profile ❌ No (None)
/users/alice/settings ❌ No (None)

This simple mechanism is powerful for:

  • Routing: Directing /users/alice/profile to the user profile handler.
  • Authorization: Checking if a user has access to a resource defined by a path.
  • Data Extraction: Getting the userID to fetch data from a database.

Features

  • 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.

Installation

go get github.com/tsdkv/pathmatch

Usage

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.

Basic Matching
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"}
Advanced Matching with Parsed Templates
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"}
Step-by-Step Traversal with Walker

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 PathTemplates 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{}
Using the WalkerBuilder

For more control over the Walker's behavior, such as setting case-insensitive matching, use the WalkerBuilder.

// Create a case-insensitive walker
builder := pathmatch.NewWalkerBuilder("/Users/Alice/Settings")
walker := builder.WithCaseIncensitive().Build()

template, _ := pathmatch.ParseTemplate("/users/{id}")
stepVars, ok, _ := walker.Step(template)
// ok is true, because matching is case-insensitive
// stepVars is map[string]string{"id": "Alice"}

Path Template Syntax

Templates must start with a /. Path segments are separated by /.

  1. Literals:

    • Exact string matches for a path segment (e.g., users, config).
    • Can contain any character except /, *, {, }.
  2. Variables:

    • Format: {variableName}
    • Acts as a placeholder for a single, dynamic path segment.
    • Example: /users/{userID} matches /users/alice and captures userID="alice".
    • Variable names must follow the same rules as literals (no /, *, {, }).
  3. 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.
  4. 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.
    • The value matched by ** is not captured as a named variable.
  5. 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 captures path="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.

TODO

  • 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]+}.
  • Fuzz testing to ensure robustness against malformed paths and templates.
  • Secutiry features, such as escaping or sanitizing paths to prevent injection attacks.
    • Add an option to limit the length of the concrete path being processed to prevent attacks with excessively long strings that consume memory.

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Clean

func Clean(path string) string

Clean normalizes a path string by removing redundant slashes and any trailing slash (unless it's the root path "/"). For example, Clean("/users//alice///") returns "/users/alice". Clean("/") returns "/".

func CompileAndMatch

func CompileAndMatch(templatePattern string, path string, opts ...MatchOption) (matched bool, vars map[string]string, err error)

CompileAndMatch parses the templatePattern string and then matches it against the given path. It's a convenience wrapper around ParseTemplate and Match.

func Join

func Join(segments ...string) string

Join combines path segments into a single path string. It ensures segments are joined by a single slash and prepends a leading slash. If no segments are provided, it returns "/". Example: Join("users", "alice", "profile") returns "/users/alice/profile".

func Match

func Match(template *pathmatchpb.PathTemplate, path string, opts ...MatchOption) (matched bool, vars map[string]string, err error)

Matches path to a parsed template path path cant contain wildcards or variables, only literal segments

/path/*/to matches /path/any/to /path/{var} matches /path/to and returns map[string]string{"var": "to"} /path/{var=**} matches /path/to/with/more and returns map[string]string{"var": "to/with/more"}

func ParseTemplate

func ParseTemplate(s string) (*pmpb.PathTemplate, error)

ParseTemplate parses a path template string into a structured PathTemplate object.

The template string must start with a '/' and may contain:

  • Literal segments (e.g., "/users")
  • Wildcard segments: '*' matches any single path segment
  • Double wildcard: '**' matches zero or more segments, but only as a full segment and only at the end
  • Variables: '{name}' for a single segment, or '{name=pattern}' where pattern is a sequence of segments

func Split

func Split(path string) []string

Split splits a path string into its segments. It handles leading/trailing slashes and multiple slashes between segments. For example, Split("/users//alice/") returns ["users", "alice"]. An empty path or a path consisting only of slashes results in an empty slice.

Types

type MatchOption added in v0.1.1

type MatchOption func(*match.MatchOptions)

func WithCaseInsensitive added in v0.1.1

func WithCaseInsensitive() MatchOption

WithCaseInsensitive sets the match options to be case-insensitive.

func WithKeepFirstVariable added in v0.1.3

func WithKeepFirstVariable() MatchOption

WithKeepFirstVariable sets the variable merging policy. If true, when a variable name is encountered more than once, the value from the first match is kept. If false (default), the last match overwrites previous values.

type Walker

type Walker struct {
	// contains filtered or unexported fields
}

Walker facilitates step-by-step traversal and matching of a concrete path against a series of path templates. It is designed for scenarios such as evaluating hierarchical configurations, where a specific path (e.g., "/users/alice/settings/profile") is incrementally matched against templates (e.g., "/users/{userID}", then "/settings/{section}").

The Walker maintains its current position within the concrete path, accumulates variables extracted from successful template matches, and tracks the depth of traversal. It supports stepping forward through matches, stepping back to previous states, and resetting to the initial state.

A Walker is typically initialized with a full concrete path using NewWalker. Subsequent calls to Step attempt to consume parts of this path according to the provided PathTemplates.

func NewWalker

func NewWalker(path string) *Walker

NewWalker creates and initializes a new Walker for the given concretePath. The walker starts at the beginning of the path with no variables captured and a depth of 0.

Example:

walker := NewWalker("/users/alice/settings/profile")

func (*Walker) Depth

func (w *Walker) Depth() int

Depth returns the number of successful Step operations performed, effectively the current "level" of matching within the path. It starts at 0 and increments with each successful Step.

func (*Walker) IsComplete

func (w *Walker) IsComplete() bool

IsComplete checks if the entire concretePath has been consumed by Step operations. It is a convenience method equivalent to checking if Remaining() returns an empty string.

func (*Walker) Remaining

func (w *Walker) Remaining() string

Remaining returns the portion of the original concretePath that has not yet been consumed by successful Step operations.

Example:

walker := NewWalker("/a/b/c")
walker.Step(templateForA) // Assuming templateForA matches "/a"
fmt.Println(walker.Remaining()) // Output: "/b/c"

func (*Walker) Reset

func (w *Walker) Reset()

Reset returns the Walker to its initial state, as if it were newly created with the original concretePath. All captured variables are cleared, the remaining path is reset to the full concrete path, and depth is set to 0. The history for StepBack is also cleared.

func (*Walker) Step

func (w *Walker) Step(template *pathmatchpb.PathTemplate) (stepVars map[string]string, matched bool, err error)

Step attempts to match the provided PathTemplate against the current beginning of the Remaining path.

If the template matches:

  • The walker's internal position advances past the matched segment(s).
  • Variables captured by this specific template match are returned in stepVars.
  • These stepVars are also merged into the walker's total Variables().
  • The walker's Depth is incremented.
  • matched is true.

If the template does not match:

  • The walker's state remains unchanged.
  • stepVars is nil.
  • matched is false.

Example:

walker := NewWalker("/users/alice/settings/profile")
userTemplate, _ := pathmatch.ParseTemplate("/users/{id}")
vars, ok := walker.Step(userTemplate)
// vars: map[string]string{"id": "alice"}, ok: true
// walker.Remaining(): "/settings/profile"
// walker.Variables(): map[string]string{"id": "alice"}
// walker.Depth(): 1

func (*Walker) StepBack

func (w *Walker) StepBack() bool

StepBack reverts the Walker to the state it was in before the last successful Step operation. This effectively "undoes" the last match.

If a StepBack is possible (i.e., Depth > 0):

  • The walker's Remaining path, Variables, and Depth are restored.
  • It returns true.

If no previous steps exist (Depth == 0), the state is unchanged, and it returns false.

Example:

walker.Step(template1) // depth becomes 1
walker.Step(template2) // depth becomes 2
ok := walker.StepBack() // ok: true, depth becomes 1
ok = walker.StepBack() // ok: true, depth becomes 0
ok = walker.StepBack() // ok: false, depth remains 0

func (*Walker) Variables

func (w *Walker) Variables() map[string]string

Variables returns a map of all variables accumulated from all successful Step operations up to the current point. The keys are variable names from the path templates, and values are the matched segments from the concrete path. The returned map is a copy; modifications to it will not affect the walker's internal state.

type WalkerBuilder added in v0.1.1

type WalkerBuilder struct {
	// contains filtered or unexported fields
}

WalkerBuilder is a helper struct for constructing a Walker instance. It allows setting options before building the Walker.

Example:

walkerBuilder := pathmatch.NewWalkerBuilder("/users/alice/settings/profile")
walker := walkerBuilder.WithCaseIncensitive().Build()

func NewWalkerBuilder added in v0.1.1

func NewWalkerBuilder(concretePath string) *WalkerBuilder

NewWalkerBuilder initializes a new WalkerBuilder with the given concrete path. The builder allows customization of match options before creating the Walker.

func (*WalkerBuilder) Build added in v0.1.1

func (b *WalkerBuilder) Build() (*Walker, error)

Build creates a new Walker instance using the concrete path and match options specified in the builder. It initializes the Walker to start at the beginning of the concrete path with no variables captured and a depth of 0.

func (*WalkerBuilder) WithCaseIncensitive added in v0.1.1

func (b *WalkerBuilder) WithCaseIncensitive() *WalkerBuilder

WithCaseIncensitive sets the match options to be case-insensitive. This modifies the Walker's behavior to ignore case when matching path segments.

func (*WalkerBuilder) WithKeepFirstVariable added in v0.1.3

func (b *WalkerBuilder) WithKeepFirstVariable() *WalkerBuilder

WithKeepFirstVariable sets the variable merging policy. If true, when a variable name is encountered more than once, the value from the first match is kept. If false (default), the last match overwrites previous values.

Directories

Path Synopsis
internal
pathmatchpb
v1

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL