A Go library for URI template expansion and matching, based on RFC 6570 with extensions.
- Provide robust URI template expansion compatible with RFC 6570 (Level 4).
- Implement URI matching and variable extraction against templates.
- Support a non-standard wildcard syntax (
{var*}) for matching multiple path segments. - Offer a clear and efficient Go API.
- Expansion: Generate URIs from templates and variables (
Template.Expand). - Matching/Extraction: Match a given URI against a template and extract variable values (
Template.Match). - Wildcard Matching: Use
{var*}in templates to match one or more path segments greedily, including/. For example,docs/{path*}/editcan matchdocs/a/b/c/edit, extractingpath = "a/b/c". - Variable Introspection: List variable names defined in a template (
Template.Varnames). - RFC 6570 Level 4: Full support for complex data types (lists and maps), multi-variable expressions, and modifiers (explode and prefix length).
- RFC 6570 Operators: Support for all operators (
+,#,.,/,;,?,&) with appropriate encoding rules. - Percent-encoding: Context-appropriate URI encoding based on operator and component.
package wilduri
// Values represents variables for expansion or results from extraction.
type Values map[string]interface{}
// Template represents a compiled URI template.
type Template struct { ... }
// New parses and compiles a template string.
// Recognizes standard {var} and RFC6570 operators, plus wildcard {var*} syntax.
func New(template string) (*Template, error)
// MustNew is like New but panics on error.
func MustNew(template string) *Template
// Expand expands the template using provided values according to RFC6570 rules.
// Undefined variables are generally omitted (standard RFC behavior).
func (t *Template) Expand(vars Values) (string, error)
// Match attempts to match the given uri against the template.
// Returns the extracted values and true if successful.
// Percent-decodes extracted values.
func (t *Template) Match(uri string) (Values, bool)
// Raw returns the original template string.
func (t *Template) Raw() string
// Varnames returns a unique, sorted list of variable names defined in the template.
func (t *Template) Varnames() []stringpackage main
import (
"fmt"
"log"
"github.com/localrivet/wilduri"
)
func main() {
// Simple expansion
simple, _ := wilduri.New("/users/{id}")
result, _ := simple.Expand(wilduri.Values{
"id": "123",
})
fmt.Println(result) // Output: /users/123
// Multiple variables
profile, _ := wilduri.New("/users/{userId}/posts/{postId}")
result, _ = profile.Expand(wilduri.Values{
"userId": "user123",
"postId": "post456",
})
fmt.Println(result) // Output: /users/user123/posts/post456
// Query parameters
search, _ := wilduri.New("/search{?q,page,limit}")
result, _ = search.Expand(wilduri.Values{
"q": "golang",
"page": 1,
"limit": 20,
})
fmt.Println(result) // Output: /search?q=golang&page=1&limit=20
// List variables
listTmpl, _ := wilduri.New("/tags{/tags*}")
result, _ = listTmpl.Expand(wilduri.Values{
"tags": []string{"go", "web", "template"},
})
fmt.Println(result) // Output: /tags/go/web/template
// Map explode
filterTmpl, _ := wilduri.New("/products{?filters*}")
result, _ = filterTmpl.Expand(wilduri.Values{
"filters": map[string]interface{}{
"category": "electronics",
"minPrice": 100,
"inStock": true,
},
})
fmt.Println(result) // Output: /products?category=electronics&inStock=true&minPrice=100
}package main
import (
"fmt"
"github.com/localrivet/wilduri"
)
func main() {
// Standard variable matching
tmpl, _ := wilduri.New("/users/{id}/profile")
values, ok := tmpl.Match("/users/123/profile")
if ok {
fmt.Printf("Matched! id = %v\n", values["id"])
// Output: Matched! id = 123
}
// Multiple variables
orderTmpl, _ := wilduri.New("/orders/{orderId}/items/{itemId}")
values, ok = orderTmpl.Match("/orders/ORD-123/items/ITEM-456")
if ok {
fmt.Printf("Order ID: %v, Item ID: %v\n", values["orderId"], values["itemId"])
// Output: Order ID: ORD-123, Item ID: ITEM-456
}
// Wildcard matching across multiple path segments
docTmpl, _ := wilduri.New("/docs/{path*}/edit")
values, ok = docTmpl.Match("/docs/project/web/homepage/edit")
if ok {
fmt.Printf("Document path: %v\n", values["path"])
// Output: Document path: project/web/homepage
}
// Percent-encoded input
searchTmpl, _ := wilduri.New("/search/{term}")
values, ok = searchTmpl.Match("/search/golang%20templates")
if ok {
fmt.Printf("Search term: %v\n", values["term"])
// Output: Search term: golang templates
}
// Route pattern matching in a web application (pseudo-code)
routes := map[string]*wilduri.Template{
"userProfile": wilduri.MustNew("/users/{userId}"),
"editPost": wilduri.MustNew("/blog/{year}/{slug}/edit"),
"apiResource": wilduri.MustNew("/api/{version}/{resource*}"),
}
// Incoming request path
path := "/api/v1/users/123/posts"
for name, pattern := range routes {
if values, ok := pattern.Match(path); ok {
fmt.Printf("Route '%s' matched! Values: %v\n", name, values)
// Output: Route 'apiResource' matched! Values: map[resource:users/123/posts version:v1]
break
}
}
}package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"context"
"github.com/localrivet/wilduri"
)
// Simple router using wilduri for pattern matching
type Router struct {
routes map[string]http.HandlerFunc
}
func NewRouter() *Router {
return &Router{
routes: make(map[string]http.HandlerFunc),
}
}
func (r *Router) Handle(pattern string, handler http.HandlerFunc) {
r.routes[pattern] = handler
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
for pattern, handler := range r.routes {
tmpl, err := wilduri.New(pattern)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid route pattern: %v", err), http.StatusInternalServerError)
return
}
if params, matched := tmpl.Match(path); matched {
// Store extracted path parameters in request context
ctx := req.Context()
for k, v := range params {
ctx = context.WithValue(ctx, k, v)
}
// Call the handler with parameters
handler(w, req.WithContext(ctx))
return
}
}
// No routes matched
http.NotFound(w, req)
}
// Usage example
func userHandler(w http.ResponseWriter, r *http.Request) {
// Extract path parameter
userID := r.Context().Value("userId").(string)
// Build response
response := map[string]string{
"userId": userID,
"name": "John Doe",
"email": "[email protected]",
}
// Write JSON response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
router := NewRouter()
// Register routes
router.Handle("/users/{userId}", userHandler)
router.Handle("/api/{version}/{resource*}", apiHandler)
// Start server
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
// Helper function to extract and use path parameters
func apiHandler(w http.ResponseWriter, r *http.Request) {
version := r.Context().Value("version").(string)
resource := r.Context().Value("resource").(string)
fmt.Fprintf(w, "API Version: %s, Resource: %s", version, resource)
}wilduri provides a comprehensive set of utility functions to simplify common URI template operations:
Centrally manage named templates for efficient reuse:
// Create a registry
registry := wilduri.NewRegistry()
// Register templates
registry.RegisterString("users", "/users/{id}")
registry.RegisterString("posts", "/posts/{postId}")
// Get and use templates
usersTmpl, ok := registry.Get("users")
expanded, err := registry.Expand("users", wilduri.Values{"id": "123"})Utilities for constructing and manipulating URLs:
// Join path segments properly
path := wilduri.JoinPath("/api", "v1", "users/") // "/api/v1/users/"
// Build a complete URL from base and template
url, err := wilduri.BuildURL("https://example.com",
"/api/{version}/users/{id}",
wilduri.Values{"version": "v1", "id": "123"})
// "https://example.com/api/v1/users/123"
// Build just the query string part
query, err := wilduri.BuildQueryParams("fields,sort,limit",
wilduri.Values{
"fields": []string{"name", "email"},
"sort": "created_at",
"limit": 10,
})
// "?fields=name,email&limit=10&sort=created_at"Seamless integration with Go's HTTP package:
// Middleware that extracts path parameters
handler := wilduri.Middleware("/users/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get path parameters
params := wilduri.GetParams(r)
userID := params["id"].(string)
fmt.Fprintf(w, "User ID: %s", userID)
}))
// Handler that directly receives path parameters
handler := wilduri.WrapHandler("/users/{id}", func(w http.ResponseWriter, r *http.Request, params wilduri.Values) {
userID := params["id"].(string)
fmt.Fprintf(w, "User ID: %s", userID)
})Type-safe functions for handling path parameters:
// Get parameters with proper type conversion and defaults
id := wilduri.GetInt(params, "id", 0)
name := wilduri.GetString(params, "name", "Anonymous")
active := wilduri.GetBool(params, "active", false)
tags := wilduri.GetStringList(params, "tags") // From comma-separated string, []string, or []interface{}Build and combine templates programmatically:
// Combine existing templates
tmpl1 := wilduri.MustNew("/api/")
tmpl2 := wilduri.MustNew("{version}/users/{id}")
combined, err := wilduri.Combine(tmpl1, tmpl2)
// "/api/{version}/users/{id}"
// Combine template strings
combined := wilduri.MustCombineStrings("/api/", "{version}", "/users/{id}")
// Fluent builder pattern
template := wilduri.NewBuilder().
Literal("/api/").
Path("version").
Literal("/users/").
PathWildcard("path").
Query("fields", "sort").
Build()
// "/api/{version}/users/{path*}{?fields,sort}"- Internal Representation: Templates are parsed into a sequence of
LiteralSegmentandVariableSegmentobjects. - Wildcard Syntax:
{name*}is used for wildcard variables. The*is part of the variable definition within the braces. - Dual Purpose Asterisk: The
*modifier serves two purposes in this library:- In matching context (
Template.Match), it enables wildcard behavior, allowing a variable to match multiple segments. - In expansion context (
Template.Expand), it functions as the RFC 6570 explode modifier, which changes how lists and maps are expanded.
- In matching context (
- Default Values: Default variable values are not supported within the template syntax (e.g., no
{id=1}or{id:1}). Applications usingwildurishould handle defaults before callingExpand. This avoids conflict with RFC 6570 syntax (:) and keeps the library focused. - RFC 6570 Operators: We support the standard RFC 6570 operators (
+,#,.,/,;,?,&) for expansion, each with its specific encoding rules. - Map Key Sorting: For deterministic output, map keys are sorted alphabetically during expansion.
- Basic API structure (
wilduri.go) - Internal segment types (
segment.go) - Parser for literals,
{var},{var*}, and RFC 6570 operators (parse.go) -
New/MustNewconstructor implemented -
Raw()implemented -
Varnames()implemented - Implement
Match(uri string) (Values, bool)- Logic to step through segments and the input URI
- Literal matches
- Standard variable matching (stops at
/or next literal) - Wildcard variable matching (greedy up to next literal)
- Percent-decoding of extracted values
- Implement
Expand(vars Values) (string, error)- Level 1 (
{var}) expansion with proper encoding - Level 2 operators (
.,/,;,?,&) with appropriate prefixing - Level 3 operators (
+,#) with appropriate encoding rules - Level 4 features (list/map expansion, multi-variable expressions, explode/prefix modifiers)
- Level 1 (
- Comprehensive tests for matching and expansion
- Parser validation for variable names and expression structure
- Full RFC 6570 Level 4 support
- List and map type expansion
- Multi-variable expressions (e.g.,
{a,b}) - Modifiers (
*explode,:prefix length)
- Performance optimizations
- Additional utility functions for common use cases
wilduri has been benchmarked on various template operations to ensure high performance:
| Operation | Operations/sec | Time/op | Memory/op | Allocs/op |
|---|---|---|---|---|
Simple (/users/{id}) |
~10 million | ~117 ns | 40 bytes | 4 |
| Multiple vars | ~4.7 million | ~259 ns | 88 bytes | 7 |
| Wildcard | ~3 million | ~409 ns | 152 bytes | 8 |
| Complex (query params) | ~700K | ~1.7 μs | 760 bytes | 37 |
| Map explode | ~1 million | ~1.1 μs | 608 bytes | 28 |
| Operation | Operations/sec | Time/op | Memory/op | Allocs/op |
|---|---|---|---|---|
| Simple | ~8.7 million | ~137 ns | 352 bytes | 3 |
| Multiple vars | ~6.1 million | ~196 ns | 368 bytes | 4 |
| Wildcard | ~4.9 million | ~215 ns | 352 bytes | 3 |
| Complex | ~4 million | ~300 ns | 368 bytes | 4 |
| Map explode | ~5.5 million | ~219 ns | 352 bytes | 3 |
| Operation | Operations/sec | Time/op | Memory/op | Allocs/op |
|---|---|---|---|---|
| Simple | ~5.5 million | ~214 ns | 256 bytes | 7 |
| Multiple vars | ~3.1 million | ~386 ns | 480 bytes | 12 |
| Wildcard | ~4.1 million | ~288 ns | 336 bytes | 9 |
| Complex | ~1.7 million | ~709 ns | 960 bytes | 17 |
| Map explode | ~4.8 million | ~245 ns | 256 bytes | 7 |
Benchmarks performed on Apple M2 Max
This project is licensed under the MIT License - see the LICENSE file for details.