wilduri

A Go library for URI template expansion and matching, based on RFC 6570 with extensions.
Goals
- 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.
Features
- 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*}/edit can match docs/a/b/c/edit, extracting path = "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.
API Overview
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() []string
Examples
Template Expansion
package 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
}
Template Matching
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
}
}
}
Web Server Integration
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": "john@example.com",
}
// 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)
}
Utility Functions
wilduri provides a comprehensive set of utility functions to simplify common URI template operations:
Template Registry
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"})
URL Building Helpers
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"
Web Framework Integration
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)
})
Enhanced Parameter Handling
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{}
Template Composition
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}"
Design Decisions
- Internal Representation: Templates are parsed into a sequence of
LiteralSegment and VariableSegment objects.
- 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.
- Default Values: Default variable values are not supported within the template syntax (e.g., no
{id=1} or {id:1}). Applications using wilduri should handle defaults before calling Expand. 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.
Implementation Status
- Basic API structure (
wilduri.go)
- Internal segment types (
segment.go)
- Parser for literals,
{var}, {var*}, and RFC 6570 operators (parse.go)
-
New/MustNew constructor 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)
- Comprehensive tests for matching and expansion
- Parser validation for variable names and expression structure
Future Enhancements
- 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:
URI Template Expansion
| 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 |
URI Template Matching
| 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 |
URI Template Parsing
| 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
License
This project is licensed under the MIT License - see the LICENSE file for details.