Skip to content

localrivet/wilduri

Repository files navigation

wilduri

Go Reference Go Report Card

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":  "[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)
}

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

  1. Basic API structure (wilduri.go)
  2. Internal segment types (segment.go)
  3. Parser for literals, {var}, {var*}, and RFC 6570 operators (parse.go)
  4. New/MustNew constructor implemented
  5. Raw() implemented
  6. Varnames() implemented
  7. 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
  8. 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)
  9. Comprehensive tests for matching and expansion
  10. Parser validation for variable names and expression structure

Future Enhancements

  1. Full RFC 6570 Level 4 support
    • List and map type expansion
    • Multi-variable expressions (e.g., {a,b})
    • Modifiers (* explode, : prefix length)
  2. Performance optimizations
  3. Additional utility functions for common use cases

Performance

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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published